forked from Qortal/qortal
Merge branch 'master' into LTCv3-merge-test
This commit is contained in:
commit
fccb3a3f0c
@ -19,10 +19,10 @@
|
|||||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||||
<ROW Property="NTP_GOOD" Value="false"/>
|
<ROW Property="NTP_GOOD" Value="false"/>
|
||||||
<ROW Property="ProductCode" Value="1033:{3F23DC7A-BC0B-4598-9FD4-C4B927A10D7F} 1049:{CF0D5DDC-7CB7-4308-8F98-DF8D2DB2D38D} 2052:{983B77E5-62CF-431C-B015-B96C5DCA6858} 2057:{BF1C757A-A3A0-4285-906A-6D8D91D74D0A} " Type="16"/>
|
<ROW Property="ProductCode" Value="1033:{F45F964E-1F1F-4B24-AAC5-687C656B5534} 1049:{F228D3BD-A49D-4332-A57D-67A5EFA47674} 2052:{17BB4192-98DA-4D79-AA29-7340ADA1EB38} 2057:{F25873D9-9179-4B35-98FB-EA8D19EE89DE} " Type="16"/>
|
||||||
<ROW Property="ProductLanguage" Value="2057"/>
|
<ROW Property="ProductLanguage" Value="2057"/>
|
||||||
<ROW Property="ProductName" Value="Qortal"/>
|
<ROW Property="ProductName" Value="Qortal"/>
|
||||||
<ROW Property="ProductVersion" Value="1.3.5" Type="32"/>
|
<ROW Property="ProductVersion" Value="1.3.7" Type="32"/>
|
||||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
@ -174,7 +174,7 @@
|
|||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_97" ComponentId="{D5544706-E2A7-424F-AEA5-3963E355AA29}" Directory_="jdk.crypto.mscapi_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_97" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_97" ComponentId="{D5544706-E2A7-424F-AEA5-3963E355AA29}" Directory_="jdk.crypto.mscapi_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_97" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_98" ComponentId="{104DBCE8-A458-4B3E-9EFA-2D8613561619}" Directory_="jdk.dynalink_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_98" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_98" ComponentId="{104DBCE8-A458-4B3E-9EFA-2D8613561619}" Directory_="jdk.dynalink_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_98" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_99" ComponentId="{D02E3C37-E81A-48FA-9E28-B26B728AECD9}" Directory_="jdk.httpserver_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_99" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_99" ComponentId="{D02E3C37-E81A-48FA-9E28-B26B728AECD9}" Directory_="jdk.httpserver_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_99" Type="0"/>
|
||||||
<ROW Component="AI_CustomARPName" ComponentId="{5FCFB67B-FDD0-4B15-9A58-2188092589E9}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
<ROW Component="AI_CustomARPName" ComponentId="{7D827076-2762-468D-BC89-813DBA8A8B89}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||||
<ROW Component="DATA_PATH" ComponentId="{EE0B6107-E244-4CDB-B195-E9038D2F1E0E}" Directory_="DATA_PATH" Attributes="0"/>
|
<ROW Component="DATA_PATH" ComponentId="{EE0B6107-E244-4CDB-B195-E9038D2F1E0E}" Directory_="DATA_PATH" Attributes="0"/>
|
||||||
|
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.ciyam</groupId>
|
||||||
|
<artifactId>AT</artifactId>
|
||||||
|
<version>1.3.8</version>
|
||||||
|
<description>POM was created from install:install-file</description>
|
||||||
|
</project>
|
@ -3,13 +3,14 @@
|
|||||||
<groupId>org.ciyam</groupId>
|
<groupId>org.ciyam</groupId>
|
||||||
<artifactId>AT</artifactId>
|
<artifactId>AT</artifactId>
|
||||||
<versioning>
|
<versioning>
|
||||||
<release>1.3.7</release>
|
<release>1.3.8</release>
|
||||||
<versions>
|
<versions>
|
||||||
<version>1.3.4</version>
|
<version>1.3.4</version>
|
||||||
<version>1.3.5</version>
|
<version>1.3.5</version>
|
||||||
<version>1.3.6</version>
|
<version>1.3.6</version>
|
||||||
<version>1.3.7</version>
|
<version>1.3.7</version>
|
||||||
|
<version>1.3.8</version>
|
||||||
</versions>
|
</versions>
|
||||||
<lastUpdated>20200812131412</lastUpdated>
|
<lastUpdated>20200925114415</lastUpdated>
|
||||||
</versioning>
|
</versioning>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
13
pom.xml
13
pom.xml
@ -3,20 +3,19 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.qortal</groupId>
|
<groupId>org.qortal</groupId>
|
||||||
<artifactId>qortal</artifactId>
|
<artifactId>qortal</artifactId>
|
||||||
<version>1.3.5</version>
|
<version>1.3.7</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<altcoinj.version>bf9fb80</altcoinj.version>
|
<altcoinj.version>bf9fb80</altcoinj.version>
|
||||||
<bitcoinj.version>0.15.6</bitcoinj.version>
|
<bitcoinj.version>0.15.6</bitcoinj.version>
|
||||||
<bouncycastle.version>1.64</bouncycastle.version>
|
<bouncycastle.version>1.64</bouncycastle.version>
|
||||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||||
<ciyam-at.version>1.3.7</ciyam-at.version>
|
<ciyam-at.version>1.3.8</ciyam-at.version>
|
||||||
<commons-net.version>3.6</commons-net.version>
|
<commons-net.version>3.6</commons-net.version>
|
||||||
<commons-text.version>1.8</commons-text.version>
|
<commons-text.version>1.8</commons-text.version>
|
||||||
<dagger.version>1.2.2</dagger.version>
|
<dagger.version>1.2.2</dagger.version>
|
||||||
<guava.version>28.1-jre</guava.version>
|
<guava.version>28.1-jre</guava.version>
|
||||||
<hsqldb.version>2.5.0-fixed</hsqldb.version>
|
<hsqldb.version>2.5.1</hsqldb.version>
|
||||||
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
|
|
||||||
<jersey.version>2.29.1</jersey.version>
|
<jersey.version>2.29.1</jersey.version>
|
||||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||||
<log4j.version>2.12.1</log4j.version>
|
<log4j.version>2.12.1</log4j.version>
|
||||||
@ -407,12 +406,6 @@
|
|||||||
<artifactId>hsqldb</artifactId>
|
<artifactId>hsqldb</artifactId>
|
||||||
<version>${hsqldb.version}</version>
|
<version>${hsqldb.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.hsqldb</groupId>
|
|
||||||
<artifactId>sqltool</artifactId>
|
|
||||||
<version>${hsqldb-sqltool.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<!-- CIYAM AT (automated transactions) -->
|
<!-- CIYAM AT (automated transactions) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ciyam</groupId>
|
<groupId>org.ciyam</groupId>
|
||||||
|
@ -5,10 +5,20 @@ import java.net.UnknownHostException;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
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) {
|
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;
|
InetAddress remoteAddr;
|
||||||
try {
|
try {
|
||||||
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||||
@ -19,4 +29,5 @@ public class Security {
|
|||||||
if (!remoteAddr.isLoopbackAddress())
|
if (!remoteAddr.isLoopbackAddress())
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.qortal.api.model;
|
package org.qortal.api.model;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -13,17 +14,61 @@ import org.qortal.transaction.Transaction.TransactionType;
|
|||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class ActivitySummary {
|
public class ActivitySummary {
|
||||||
|
|
||||||
public int blockCount;
|
private int blockCount;
|
||||||
public int transactionCount;
|
private int assetsIssued;
|
||||||
public int assetsIssued;
|
private int namesRegistered;
|
||||||
public int namesRegistered;
|
|
||||||
|
|
||||||
// Assuming TransactionType values are contiguous so 'length' equals count
|
// Assuming TransactionType values are contiguous so 'length' equals count
|
||||||
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
|
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
|
||||||
public Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
private Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
||||||
|
private int totalTransactionCount = 0;
|
||||||
|
|
||||||
public ActivitySummary() {
|
public ActivitySummary() {
|
||||||
// Needed for JAXB
|
// 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<TransactionType, Integer> 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<TransactionType, Integer> transactionCountByType) {
|
||||||
|
this.transactionCountByType = new EnumMap<>(transactionCountByType);
|
||||||
|
|
||||||
|
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ public class NodeInfo {
|
|||||||
public String buildVersion;
|
public String buildVersion;
|
||||||
public long buildTimestamp;
|
public long buildTimestamp;
|
||||||
public String nodeId;
|
public String nodeId;
|
||||||
|
public boolean isTestNet;
|
||||||
|
|
||||||
public NodeInfo() {
|
public NodeInfo() {
|
||||||
}
|
}
|
||||||
|
@ -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.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@ -473,6 +474,7 @@ public class AddressesResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String computePublicize(String rawBytes58) {
|
public String computePublicize(String rawBytes58) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
@ -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.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -40,7 +41,6 @@ import org.qortal.account.Account;
|
|||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
import org.qortal.api.ApiException;
|
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
import org.qortal.api.model.ActivitySummary;
|
import org.qortal.api.model.ActivitySummary;
|
||||||
@ -57,6 +57,7 @@ import org.qortal.network.PeerAddress;
|
|||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@ -118,6 +119,7 @@ public class AdminResource {
|
|||||||
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
|
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
|
||||||
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
||||||
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
||||||
|
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
|
||||||
|
|
||||||
return nodeInfo;
|
return nodeInfo;
|
||||||
}
|
}
|
||||||
@ -132,6 +134,7 @@ public class AdminResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public NodeStatus status() {
|
public NodeStatus status() {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -152,6 +155,7 @@ public class AdminResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String shutdown() {
|
public String shutdown() {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -180,7 +184,10 @@ public class AdminResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public ActivitySummary summary() {
|
public ActivitySummary summary() {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
ActivitySummary summary = new ActivitySummary();
|
ActivitySummary summary = new ActivitySummary();
|
||||||
|
|
||||||
LocalDate date = LocalDate.now();
|
LocalDate date = LocalDate.now();
|
||||||
@ -192,16 +199,13 @@ public class AdminResource {
|
|||||||
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
|
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
|
||||||
int endHeight = repository.getBlockRepository().getBlockchainHeight();
|
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.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
|
||||||
summary.transactionCount += count;
|
|
||||||
|
|
||||||
summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size();
|
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
|
||||||
|
|
||||||
summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size();
|
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
} catch (DataException e) {
|
} 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
|
@GET
|
||||||
@Path("/mintingaccounts")
|
@Path("/mintingaccounts")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -221,6 +249,7 @@ public class AdminResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public List<MintingAccountData> getMintingAccounts() {
|
public List<MintingAccountData> getMintingAccounts() {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -267,6 +296,7 @@ public class AdminResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String addMintingAccount(String seed58) {
|
public String addMintingAccount(String seed58) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -319,6 +349,7 @@ public class AdminResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String deleteMintingAccount(String key58) {
|
public String deleteMintingAccount(String key58) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -418,6 +449,7 @@ public class AdminResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String orphan(String targetHeightString) {
|
public String orphan(String targetHeightString) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -435,8 +467,6 @@ public class AdminResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
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})
|
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String forceSync(String targetPeerAddress) {
|
public String forceSync(String targetPeerAddress) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -492,8 +523,6 @@ public class AdminResource {
|
|||||||
return syncResult.name();
|
return syncResult.name();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
} catch (ApiException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
} catch (InterruptedException e) {
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
package org.qortal.api.resource;
|
package org.qortal.api.resource;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
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.Extension;
|
||||||
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
|
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
|
||||||
import io.swagger.v3.oas.annotations.info.Info;
|
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import org.qortal.api.Security;
|
||||||
|
|
||||||
@OpenAPIDefinition(
|
@OpenAPIDefinition(
|
||||||
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
|
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
|
||||||
tags = {
|
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 {
|
public class ApiDefinition {
|
||||||
}
|
}
|
@ -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.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -156,6 +157,7 @@ public class ChatResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String buildChat(ChatTransactionData transactionData) {
|
public String buildChat(ChatTransactionData transactionData) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -203,6 +205,7 @@ public class ChatResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String buildChat(String rawBytes58) {
|
public String buildChat(String rawBytes58) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
@ -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.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
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})
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String cancelTrade(CrossChainCancelRequest cancelRequest) {
|
public String cancelTrade(CrossChainCancelRequest cancelRequest) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
@ -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.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -26,10 +29,17 @@ import org.qortal.api.ApiException;
|
|||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
import org.qortal.api.model.ConnectedPeer;
|
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.data.network.PeerData;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
|
import org.qortal.network.Peer;
|
||||||
import org.qortal.network.PeerAddress;
|
import org.qortal.network.PeerAddress;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.utils.ExecuteProduceConsume;
|
import org.qortal.utils.ExecuteProduceConsume;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@ -122,6 +132,7 @@ public class PeersResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
|
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -159,6 +170,7 @@ public class PeersResource {
|
|||||||
@ApiErrors({
|
@ApiErrors({
|
||||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||||
})
|
})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String addPeer(String address) {
|
public String addPeer(String address) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -213,6 +225,7 @@ public class PeersResource {
|
|||||||
@ApiErrors({
|
@ApiErrors({
|
||||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||||
})
|
})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String removePeer(String address) {
|
public String removePeer(String address) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
@ -248,6 +261,7 @@ public class PeersResource {
|
|||||||
@ApiErrors({
|
@ApiErrors({
|
||||||
ApiError.REPOSITORY_ISSUE
|
ApiError.REPOSITORY_ISSUE
|
||||||
})
|
})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
public String removeKnownPeers(String address) {
|
public String removeKnownPeers(String address) {
|
||||||
Security.checkApiCallAllowed(request);
|
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<BlockSummaryData> 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<Peer> 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<BlockSummaryData> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -257,7 +257,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
|||||||
|
|
||||||
if (crossChainTradeData.mode == AcctMode.OFFERING)
|
if (crossChainTradeData.mode == AcctMode.OFFERING)
|
||||||
// We want when trade was created, not when it was last updated
|
// We want when trade was created, not when it was last updated
|
||||||
atStateTimestamp = atState.getCreation();
|
atStateTimestamp = crossChainTradeData.creationTimestamp;
|
||||||
else
|
else
|
||||||
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||||
|
|
||||||
|
@ -51,16 +51,17 @@ public class AT {
|
|||||||
|
|
||||||
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
|
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.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
|
||||||
machineState.isFrozen(), machineState.getFrozenBalance());
|
machineState.isFrozen(), machineState.getFrozenBalance());
|
||||||
|
|
||||||
byte[] stateData = machineState.toBytes();
|
byte[] stateData = machineState.toBytes();
|
||||||
byte[] stateHash = Crypto.digest(stateData);
|
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
|
// Getters / setters
|
||||||
@ -106,12 +107,11 @@ public class AT {
|
|||||||
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
|
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
long creation = this.atData.getCreation();
|
|
||||||
byte[] stateData = state.toBytes();
|
byte[] stateData = state.toBytes();
|
||||||
byte[] stateHash = Crypto.digest(stateData);
|
byte[] stateHash = Crypto.digest(stateData);
|
||||||
long atFees = api.calcFinalFees(state);
|
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();
|
return api.getTransactions();
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import static java.util.stream.Collectors.toMap;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.NumberFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -15,6 +17,7 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.Level;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
@ -791,15 +794,46 @@ public class Block {
|
|||||||
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
|
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries) {
|
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries, int maxHeight) {
|
||||||
BigInteger cumulativeWeight = BigInteger.ZERO;
|
BigInteger cumulativeWeight = BigInteger.ZERO;
|
||||||
int parentHeight = commonBlockHeight;
|
int parentHeight = commonBlockHeight;
|
||||||
byte[] parentBlockSignature = commonBlockSignature;
|
byte[] parentBlockSignature = commonBlockSignature;
|
||||||
|
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||||
|
boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE);
|
||||||
|
|
||||||
for (BlockSummaryData blockSummaryData : blockSummaries) {
|
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();
|
parentHeight = blockSummaryData.getHeight();
|
||||||
parentBlockSignature = blockSummaryData.getSignature();
|
parentBlockSignature = blockSummaryData.getSignature();
|
||||||
|
|
||||||
|
/* Potential future consensus change: only comparing the same number of blocks.
|
||||||
|
if (parentHeight >= maxHeight)
|
||||||
|
break;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
return cumulativeWeight;
|
return cumulativeWeight;
|
||||||
|
@ -531,6 +531,7 @@ public class BlockChain {
|
|||||||
|
|
||||||
private static void rebuildBlockchain() throws DataException {
|
private static void rebuildBlockchain() throws DataException {
|
||||||
// (Re)build repository
|
// (Re)build repository
|
||||||
|
if (!RepositoryManager.wasPristineAtOpen())
|
||||||
RepositoryManager.rebuild();
|
RepositoryManager.rebuild();
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
@ -567,7 +568,8 @@ public class BlockChain {
|
|||||||
--height;
|
--height;
|
||||||
orphanBlockData = repository.getBlockRepository().fromHeight(height);
|
orphanBlockData = repository.getBlockRepository().fromHeight(height);
|
||||||
|
|
||||||
Controller.getInstance().onNewBlock(orphanBlockData);
|
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||||
|
Controller.getInstance().onOrphanedBlock(orphanBlockData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
77
src/main/java/org/qortal/controller/AtStatesTrimmer.java
Normal file
77
src/main/java/org/qortal/controller/AtStatesTrimmer.java
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -306,8 +306,10 @@ public class BlockMinter extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newBlockMinted) {
|
if (newBlockMinted) {
|
||||||
BlockData newBlockData = newBlock.getBlockData();
|
|
||||||
// Notify Controller and broadcast our new chain to network
|
// 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);
|
Controller.getInstance().onNewBlock(newBlockData);
|
||||||
|
|
||||||
Network network = Network.getInstance();
|
Network network = Network.getInstance();
|
||||||
|
@ -16,16 +16,24 @@ import java.util.Collections;
|
|||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
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.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
@ -87,6 +95,7 @@ import org.qortal.transaction.Transaction.TransactionType;
|
|||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.ByteArray;
|
import org.qortal.utils.ByteArray;
|
||||||
|
import org.qortal.utils.DaemonThreadFactory;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
import org.qortal.utils.Triple;
|
import org.qortal.utils.Triple;
|
||||||
|
|
||||||
@ -134,11 +143,24 @@ public class Controller extends Thread {
|
|||||||
private ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
private ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||||
private volatile boolean notifyGroupMembershipChange = false;
|
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<BlockData> latestBlocks = new LinkedList<>();
|
||||||
|
|
||||||
|
/** Cache of BlockMessages, indexed by block signature */
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private final LinkedHashMap<ByteArray, BlockMessage> blockMessageCache = new LinkedHashMap<>() {
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<ByteArray, BlockMessage> eldest) {
|
||||||
|
return this.size() > BLOCK_CACHE_SIZE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private long repositoryBackupTimestamp = startTime; // ms
|
private long repositoryBackupTimestamp = startTime; // ms
|
||||||
|
private long repositoryCheckpointTimestamp = startTime; // ms
|
||||||
private long ntpCheckTimestamp = startTime; // ms
|
private long ntpCheckTimestamp = startTime; // ms
|
||||||
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
||||||
|
|
||||||
private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
|
private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
|
||||||
|
|
||||||
/** Whether we can mint new blocks, as reported by BlockMinter. */
|
/** 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 */
|
/** Cache of latest blocks' online accounts */
|
||||||
Deque<List<OnlineAccountData>> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
|
Deque<List<OnlineAccountData>> 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
|
// Constructors
|
||||||
|
|
||||||
private Controller(String[] args) {
|
private Controller(String[] args) {
|
||||||
@ -236,21 +299,36 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
/** Returns current blockchain height, or 0 if it's not available. */
|
/** Returns current blockchain height, or 0 if it's not available. */
|
||||||
public int getChainHeight() {
|
public int getChainHeight() {
|
||||||
BlockData blockData = this.chainTip;
|
synchronized (this.latestBlocks) {
|
||||||
|
BlockData blockData = this.latestBlocks.peekLast();
|
||||||
if (blockData == null)
|
if (blockData == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return blockData.getHeight();
|
return blockData.getHeight();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns highest block, or null if it's not available. */
|
/** Returns highest block, or null if it's not available. */
|
||||||
public BlockData getChainTip() {
|
public BlockData getChainTip() {
|
||||||
return this.chainTip;
|
synchronized (this.latestBlocks) {
|
||||||
|
return this.latestBlocks.peekLast();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cache new blockchain tip. */
|
public void refillLatestBlocksCache() throws DataException {
|
||||||
public void setChainTip(BlockData blockData) {
|
// Set initial chain height/tip
|
||||||
this.chainTip = blockData;
|
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() {
|
public ReentrantLock getBlockchainLock() {
|
||||||
@ -332,13 +410,8 @@ public class Controller extends Thread {
|
|||||||
try {
|
try {
|
||||||
BlockChain.validate();
|
BlockChain.validate();
|
||||||
|
|
||||||
// Set initial chain height/tip
|
Controller.getInstance().refillLatestBlocksCache();
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
|
||||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
|
||||||
|
|
||||||
Controller.getInstance().setChainTip(blockData);
|
|
||||||
LOGGER.info(String.format("Our chain height at start-up: %d", blockData.getHeight()));
|
|
||||||
}
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
LOGGER.error("Couldn't validate blockchain", e);
|
LOGGER.error("Couldn't validate blockchain", e);
|
||||||
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
||||||
@ -413,6 +486,11 @@ public class Controller extends Thread {
|
|||||||
Thread.currentThread().setName("Controller");
|
Thread.currentThread().setName("Controller");
|
||||||
|
|
||||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
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 {
|
try {
|
||||||
while (!isStopping) {
|
while (!isStopping) {
|
||||||
@ -454,6 +532,18 @@ public class Controller extends Thread {
|
|||||||
final long requestMinimumTimestamp = now - ARBITRARY_REQUEST_TIMEOUT;
|
final long requestMinimumTimestamp = now - ARBITRARY_REQUEST_TIMEOUT;
|
||||||
arbitraryDataRequests.entrySet().removeIf(entry -> entry.getValue().getC() < requestMinimumTimestamp);
|
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)
|
// Give repository a chance to backup (if enabled)
|
||||||
if (repositoryBackupInterval > 0 && now >= repositoryBackupTimestamp + repositoryBackupInterval) {
|
if (repositoryBackupInterval > 0 && now >= repositoryBackupTimestamp + repositoryBackupInterval) {
|
||||||
repositoryBackupTimestamp = now + repositoryBackupInterval;
|
repositoryBackupTimestamp = now + repositoryBackupInterval;
|
||||||
@ -486,7 +576,17 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
// Clear interrupted flag so we can shutdown trim threads
|
||||||
|
Thread.interrupted();
|
||||||
// Fall-through to exit
|
// 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 {
|
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||||
boolean hasStatusChanged = false;
|
boolean hasStatusChanged = false;
|
||||||
|
BlockData priorChainTip = this.getChainTip();
|
||||||
|
|
||||||
synchronized (this.syncLock) {
|
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
|
// Only update SysTray if we're potentially changing height
|
||||||
if (this.syncPercent < 100) {
|
if (this.syncPercent < 100) {
|
||||||
@ -570,8 +671,6 @@ public class Controller extends Thread {
|
|||||||
if (hasStatusChanged)
|
if (hasStatusChanged)
|
||||||
updateSysTray();
|
updateSysTray();
|
||||||
|
|
||||||
BlockData priorChainTip = this.chainTip;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||||
switch (syncResult) {
|
switch (syncResult) {
|
||||||
@ -640,9 +739,6 @@ public class Controller extends Thread {
|
|||||||
// Reset our cache of inferior chains
|
// Reset our cache of inferior chains
|
||||||
inferiorChainSignatures.clear();
|
inferiorChainSignatures.clear();
|
||||||
|
|
||||||
// Update chain-tip, systray, notify peers, websockets, etc.
|
|
||||||
this.onNewBlock(newChainTip);
|
|
||||||
|
|
||||||
Network network = Network.getInstance();
|
Network network = Network.getInstance();
|
||||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
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.
|
||||||
|
* <p>
|
||||||
|
* See <b>WARNING</b> for {@link EventBus#notify(Event)}
|
||||||
|
* to prevent deadlocks.
|
||||||
|
*/
|
||||||
public void onNewBlock(BlockData latestBlockData) {
|
public void onNewBlock(BlockData latestBlockData) {
|
||||||
// Protective copy
|
// Protective copy
|
||||||
BlockData blockDataCopy = new BlockData(latestBlockData);
|
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.
|
||||||
|
* <p>
|
||||||
|
* See <b>WARNING</b> 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<BlockData, Event> eventConstructor) {
|
||||||
requestSysTrayUpdate = true;
|
requestSysTrayUpdate = true;
|
||||||
|
|
||||||
// Notify listeners, trade-bot, etc.
|
// Notify listeners, trade-bot, etc.
|
||||||
EventBus.INSTANCE.notify(new NewBlockEvent(blockDataCopy));
|
EventBus.INSTANCE.notify(eventConstructor.apply(blockDataCopy));
|
||||||
|
|
||||||
if (this.notifyGroupMembershipChange) {
|
if (this.notifyGroupMembershipChange) {
|
||||||
this.notifyGroupMembershipChange = false;
|
this.notifyGroupMembershipChange = false;
|
||||||
@ -935,11 +1125,31 @@ public class Controller extends Thread {
|
|||||||
private void onNetworkGetBlockMessage(Peer peer, Message message) {
|
private void onNetworkGetBlockMessage(Peer peer, Message message) {
|
||||||
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
|
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
|
||||||
byte[] signature = getBlockMessage.getSignature();
|
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()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||||
|
|
||||||
if (blockData == null) {
|
if (blockData == null) {
|
||||||
// We don't have this block
|
// 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
|
// 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)));
|
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);
|
Block block = new Block(repository, blockData);
|
||||||
|
|
||||||
Message blockMessage = new BlockMessage(block);
|
BlockMessage blockMessage = new BlockMessage(block);
|
||||||
blockMessage.setId(message.getId());
|
blockMessage.setId(message.getId());
|
||||||
|
|
||||||
|
// This call also causes the other needed data to be pulled in from repository
|
||||||
if (!peer.sendMessage(blockMessage))
|
if (!peer.sendMessage(blockMessage))
|
||||||
peer.disconnect("failed to send block");
|
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) {
|
} catch (DataException e) {
|
||||||
LOGGER.error(String.format("Repository issue while send block %s to peer %s", Base58.encode(signature), peer), 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) {
|
private void onNetworkGetBlockSummariesMessage(Peer peer, Message message) {
|
||||||
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
|
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
|
||||||
byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
|
final byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
|
||||||
|
this.stats.getBlockSummariesStats.requests.incrementAndGet();
|
||||||
|
|
||||||
|
// If peer's parent signature matches our latest block signature
|
||||||
|
// then we can short-circuit with an empty response
|
||||||
|
BlockData chainTip = getChainTip();
|
||||||
|
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
||||||
|
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||||
|
blockSummariesMessage.setId(message.getId());
|
||||||
|
if (!peer.sendMessage(blockSummariesMessage))
|
||||||
|
peer.disconnect("failed to send block summaries");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||||
|
|
||||||
|
// Attempt to serve from our cache of latest blocks
|
||||||
|
synchronized (this.latestBlocks) {
|
||||||
|
blockSummaries = this.latestBlocks.stream()
|
||||||
|
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
|
||||||
|
.map(BlockSummaryData::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockSummaries.isEmpty()) {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
|
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
|
||||||
|
|
||||||
do {
|
|
||||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||||
|
|
||||||
if (blockData == null)
|
while (blockData != null && blockSummaries.size() < numberRequested) {
|
||||||
// No more blocks to send to peer
|
|
||||||
break;
|
|
||||||
|
|
||||||
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
|
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
|
||||||
blockSummaries.add(blockSummary);
|
blockSummaries.add(blockSummary);
|
||||||
parentSignature = blockData.getSignature();
|
|
||||||
} while (blockSummaries.size() < numberRequested);
|
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);
|
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
|
||||||
blockSummariesMessage.setId(message.getId());
|
blockSummariesMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(blockSummariesMessage))
|
if (!peer.sendMessage(blockSummariesMessage))
|
||||||
peer.disconnect("failed to send block summaries");
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) {
|
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) {
|
||||||
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
|
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
|
||||||
byte[] parentSignature = getSignaturesMessage.getParentSignature();
|
final byte[] parentSignature = getSignaturesMessage.getParentSignature();
|
||||||
|
this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet();
|
||||||
|
|
||||||
|
// If peer's parent signature matches our latest block signature
|
||||||
|
// then we can short-circuit with an empty response
|
||||||
|
BlockData chainTip = getChainTip();
|
||||||
|
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
||||||
|
Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
|
||||||
|
signaturesMessage.setId(message.getId());
|
||||||
|
if (!peer.sendMessage(signaturesMessage))
|
||||||
|
peer.disconnect("failed to send signatures (v2)");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
List<byte[]> signatures = new ArrayList<>();
|
List<byte[]> signatures = new ArrayList<>();
|
||||||
|
|
||||||
do {
|
// 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);
|
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||||
|
|
||||||
if (blockData == null)
|
while (blockData != null && signatures.size() < numberRequested) {
|
||||||
// No more signatures to send to peer
|
signatures.add(blockData.getSignature());
|
||||||
break;
|
|
||||||
|
|
||||||
parentSignature = blockData.getSignature();
|
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
|
||||||
signatures.add(parentSignature);
|
}
|
||||||
} while (signatures.size() < getSignaturesMessage.getNumberRequested());
|
} 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);
|
Message signaturesMessage = new SignaturesMessage(signatures);
|
||||||
signaturesMessage.setId(message.getId());
|
signaturesMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(signaturesMessage))
|
if (!peer.sendMessage(signaturesMessage))
|
||||||
peer.disconnect("failed to send signatures (v2)");
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNetworkHeightV2Message(Peer peer, Message message) {
|
private void onNetworkHeightV2Message(Peer peer, Message message) {
|
||||||
@ -1406,26 +1676,6 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
// Refresh our online accounts signatures?
|
// Refresh our online accounts signatures?
|
||||||
sendOurOnlineAccountsInfo();
|
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() {
|
private void sendOurOnlineAccountsInfo() {
|
||||||
@ -1687,4 +1937,8 @@ public class Controller extends Thread {
|
|||||||
return now - offset;
|
return now - offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StatsSnapshot getStatsSnapshot() {
|
||||||
|
return this.stats;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -175,7 +175,7 @@ public class Synchronizer {
|
|||||||
* @throws DataException
|
* @throws DataException
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
private SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon) throws DataException, InterruptedException {
|
public SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon) throws DataException, InterruptedException {
|
||||||
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
|
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
|
||||||
// Failing that, back off exponentially
|
// Failing that, back off exponentially
|
||||||
int step = INITIAL_BLOCK_STEP;
|
int step = INITIAL_BLOCK_STEP;
|
||||||
@ -313,18 +313,21 @@ public class Synchronizer {
|
|||||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
|
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
|
||||||
|
|
||||||
// Populate minter account levels for both lists of block summaries
|
// Populate minter account levels for both lists of block summaries
|
||||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
|
||||||
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
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.
|
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
|
||||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
|
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
|
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 our blockchain has greater weight then don't synchronize with peer
|
||||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||||
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
|
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;
|
return SynchronizationResult.INFERIOR_CHAIN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,12 +408,15 @@ public class Synchronizer {
|
|||||||
Block block = new Block(repository, orphanBlockData);
|
Block block = new Block(repository, orphanBlockData);
|
||||||
block.orphan();
|
block.orphan();
|
||||||
|
|
||||||
|
LOGGER.trace(String.format("Orphaned block height %d, sig %.8s", ourHeight, Base58.encode(orphanBlockData.getSignature())));
|
||||||
|
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
|
|
||||||
--ourHeight;
|
--ourHeight;
|
||||||
orphanBlockData = repository.getBlockRepository().fromHeight(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));
|
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();
|
newBlock.process();
|
||||||
|
|
||||||
|
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
|
||||||
|
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
|
|
||||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||||
@ -513,6 +521,8 @@ public class Synchronizer {
|
|||||||
|
|
||||||
newBlock.process();
|
newBlock.process();
|
||||||
|
|
||||||
|
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
|
||||||
|
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
|
|
||||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||||
|
@ -610,38 +610,26 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*
|
|
||||||
* @param repository
|
|
||||||
* @param atAddress
|
|
||||||
* @throws DataException
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
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.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*
|
|
||||||
* @param repository
|
|
||||||
* @param atAddress
|
|
||||||
* @throws DataException
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* 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
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -649,7 +637,7 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
|
|
||||||
tradeData.qortalAtAddress = atAddress;
|
tradeData.qortalAtAddress = atAddress;
|
||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = atStateData.getCreation();
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
@ -561,38 +561,26 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*
|
|
||||||
* @param repository
|
|
||||||
* @param atAddress
|
|
||||||
* @throws DataException
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
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.
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
*
|
|
||||||
* @param repository
|
|
||||||
* @param atAddress
|
|
||||||
* @throws DataException
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
* 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
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -600,7 +588,7 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
|
|
||||||
tradeData.qortalAtAddress = atAddress;
|
tradeData.qortalAtAddress = atAddress;
|
||||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
tradeData.creationTimestamp = atStateData.getCreation();
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
Account atAccount = new Account(repository, atAddress);
|
Account atAccount = new Account(repository, atAddress);
|
||||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.qortal.crypto;
|
package org.qortal.crypto;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Arrays;
|
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.
|
* Returns 32-byte digest of two rounds of SHA-256 on message passed in input.
|
||||||
*
|
*
|
||||||
|
@ -5,7 +5,6 @@ public class ATStateData {
|
|||||||
// Properties
|
// Properties
|
||||||
private String ATAddress;
|
private String ATAddress;
|
||||||
private Integer height;
|
private Integer height;
|
||||||
private Long creation;
|
|
||||||
private byte[] stateData;
|
private byte[] stateData;
|
||||||
private byte[] stateHash;
|
private byte[] stateHash;
|
||||||
private Long fees;
|
private Long fees;
|
||||||
@ -14,10 +13,9 @@ public class ATStateData {
|
|||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/** Create new ATStateData */
|
/** 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.ATAddress = ATAddress;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.creation = creation;
|
|
||||||
this.stateData = stateData;
|
this.stateData = stateData;
|
||||||
this.stateHash = stateHash;
|
this.stateHash = stateHash;
|
||||||
this.fees = fees;
|
this.fees = fees;
|
||||||
@ -26,21 +24,21 @@ public class ATStateData {
|
|||||||
|
|
||||||
/** For recreating per-block ATStateData from repository where not all info is needed */
|
/** 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) {
|
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 */
|
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||||
public ATStateData(String ATAddress, byte[] stateHash) {
|
public ATStateData(String ATAddress, byte[] stateHash) {
|
||||||
// This won't ever be initial AT state from deployment as that's never serialized over the network,
|
// 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.
|
// 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 */
|
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||||
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
|
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,
|
// 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.
|
// 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
|
// Getters / setters
|
||||||
@ -58,10 +56,6 @@ public class ATStateData {
|
|||||||
this.height = height;
|
this.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getCreation() {
|
|
||||||
return this.creation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getStateData() {
|
public byte[] getStateData() {
|
||||||
return this.stateData;
|
return this.stateData;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,21 @@ public enum EventBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b>WARNING:</b> before calling this method,
|
||||||
|
* make sure repository holds no locks, e.g. by calling
|
||||||
|
* <tt>repository.discardChanges()</tt>.
|
||||||
|
* <p>
|
||||||
|
* This is because event listeners might open a new
|
||||||
|
* repository session which will deadlock HSQLDB
|
||||||
|
* if it tries to CHECKPOINT.
|
||||||
|
* <p>
|
||||||
|
* 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) {
|
public void notify(Event event) {
|
||||||
List<Listener> clonedListeners;
|
List<Listener> clonedListeners;
|
||||||
|
|
||||||
|
@ -10,12 +10,12 @@ import java.util.Set;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
public enum Translator {
|
public enum Translator {
|
||||||
INSTANCE;
|
INSTANCE;
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
|
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
|
||||||
private static final String DEFAULT_LANG = Locale.getDefault().getLanguage();
|
|
||||||
|
|
||||||
private static final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
|
private static final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ public enum Translator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String translate(String className, String key) {
|
public String translate(String className, String key) {
|
||||||
return this.translate(className, DEFAULT_LANG, key);
|
return this.translate(className, Settings.getInstance().getLocaleLang(), key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> keySet(String className, String lang) {
|
public Set<String> keySet(String className, String lang) {
|
||||||
|
@ -33,6 +33,7 @@ import org.qortal.settings.Settings;
|
|||||||
import org.qortal.utils.ExecuteProduceConsume;
|
import org.qortal.utils.ExecuteProduceConsume;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.google.common.net.InetAddresses;
|
import com.google.common.net.InetAddresses;
|
||||||
|
|
||||||
@ -348,21 +349,37 @@ public class Peer {
|
|||||||
if (this.byteBuffer == null)
|
if (this.byteBuffer == null)
|
||||||
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
|
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
|
||||||
|
|
||||||
|
final int priorPosition = this.byteBuffer.position();
|
||||||
final int bytesRead = this.socketChannel.read(this.byteBuffer);
|
final int bytesRead = this.socketChannel.read(this.byteBuffer);
|
||||||
if (bytesRead == -1) {
|
if (bytesRead == -1) {
|
||||||
this.disconnect("EOF");
|
this.disconnect("EOF");
|
||||||
return;
|
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();
|
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
// Can we build a message from buffer now?
|
// Can we build a message from buffer now?
|
||||||
|
ByteBuffer readOnlyBuffer = this.byteBuffer.asReadOnlyBuffer().flip();
|
||||||
try {
|
try {
|
||||||
message = Message.fromByteBuffer(this.byteBuffer);
|
message = Message.fromByteBuffer(readOnlyBuffer);
|
||||||
} catch (MessageException e) {
|
} catch (MessageException e) {
|
||||||
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
|
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
|
||||||
this.disconnect(e.getMessage());
|
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));
|
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<Message> queue = this.replyQueues.get(message.getId());
|
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
|
||||||
if (queue != null) {
|
if (queue != null) {
|
||||||
// Adding message to queue will unblock thread waiting for response
|
// Adding message to queue will unblock thread waiting for response
|
||||||
@ -399,7 +423,7 @@ public class Peer {
|
|||||||
|
|
||||||
// Add message to pending queue
|
// Add message to pending queue
|
||||||
if (!this.pendingMessages.offer(message)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,10 +478,24 @@ public class Peer {
|
|||||||
while (outputBuffer.hasRemaining()) {
|
while (outputBuffer.hasRemaining()) {
|
||||||
int bytesWritten = this.socketChannel.write(outputBuffer);
|
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)
|
if (bytesWritten == 0)
|
||||||
// Underlying socket's internal buffer probably full,
|
// Underlying socket's internal buffer probably full,
|
||||||
// so wait a short while for bytes to actually be transmitted over the wire
|
// 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) {
|
} catch (MessageException e) {
|
||||||
|
@ -34,6 +34,7 @@ public class BlockMessage extends Message {
|
|||||||
super(MessageType.BLOCK);
|
super(MessageType.BLOCK);
|
||||||
|
|
||||||
this.block = block;
|
this.block = block;
|
||||||
|
this.blockData = block.getBlockData();
|
||||||
this.height = block.getBlockData().getHeight();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -160,80 +160,72 @@ public abstract class Message {
|
|||||||
/**
|
/**
|
||||||
* Attempt to read a message from byte buffer.
|
* Attempt to read a message from byte buffer.
|
||||||
*
|
*
|
||||||
* @param byteBuffer
|
* @param readOnlyBuffer
|
||||||
* @return null if no complete message can be read
|
* @return null if no complete message can be read
|
||||||
* @throws MessageException
|
* @throws MessageException
|
||||||
*/
|
*/
|
||||||
public static Message fromByteBuffer(ByteBuffer byteBuffer) throws MessageException {
|
public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
|
||||||
try {
|
try {
|
||||||
byteBuffer.flip();
|
|
||||||
|
|
||||||
ByteBuffer readBuffer = byteBuffer.asReadOnlyBuffer();
|
|
||||||
|
|
||||||
// Read only enough bytes to cover Message "magic" preamble
|
// Read only enough bytes to cover Message "magic" preamble
|
||||||
byte[] messageMagic = new byte[MAGIC_LENGTH];
|
byte[] messageMagic = new byte[MAGIC_LENGTH];
|
||||||
readBuffer.get(messageMagic);
|
readOnlyBuffer.get(messageMagic);
|
||||||
|
|
||||||
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
|
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
|
||||||
// Didn't receive correct Message "magic"
|
// Didn't receive correct Message "magic"
|
||||||
throw new MessageException("Received incorrect message 'magic'");
|
throw new MessageException("Received incorrect message 'magic'");
|
||||||
|
|
||||||
// Find supporting object
|
// Find supporting object
|
||||||
int typeValue = readBuffer.getInt();
|
int typeValue = readOnlyBuffer.getInt();
|
||||||
MessageType messageType = MessageType.valueOf(typeValue);
|
MessageType messageType = MessageType.valueOf(typeValue);
|
||||||
if (messageType == null)
|
if (messageType == null)
|
||||||
// Unrecognised message type
|
// Unrecognised message type
|
||||||
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
|
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
|
||||||
|
|
||||||
// Optional message ID
|
// Optional message ID
|
||||||
byte hasId = readBuffer.get();
|
byte hasId = readOnlyBuffer.get();
|
||||||
int id = -1;
|
int id = -1;
|
||||||
if (hasId != 0) {
|
if (hasId != 0) {
|
||||||
id = readBuffer.getInt();
|
id = readOnlyBuffer.getInt();
|
||||||
|
|
||||||
if (id <= 0)
|
if (id <= 0)
|
||||||
// Invalid ID
|
// Invalid ID
|
||||||
throw new MessageException("Invalid negative ID");
|
throw new MessageException("Invalid negative ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
int dataSize = readBuffer.getInt();
|
int dataSize = readOnlyBuffer.getInt();
|
||||||
|
|
||||||
if (dataSize > MAX_DATA_SIZE)
|
if (dataSize > MAX_DATA_SIZE)
|
||||||
// Too large
|
// Too large
|
||||||
throw new MessageException(String.format("Declared data length %d larger than max allowed %d", dataSize, MAX_DATA_SIZE));
|
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;
|
ByteBuffer dataSlice = null;
|
||||||
if (dataSize > 0) {
|
if (dataSize > 0) {
|
||||||
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
|
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
|
||||||
readBuffer.get(expectedChecksum);
|
readOnlyBuffer.get(expectedChecksum);
|
||||||
|
|
||||||
// Remember this position in readBuffer so we can pass to Message subclass
|
// Slice data in readBuffer so we can pass to Message subclass
|
||||||
dataSlice = readBuffer.slice();
|
dataSlice = readOnlyBuffer.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
|
|
||||||
dataSlice.limit(dataSize);
|
dataSlice.limit(dataSize);
|
||||||
|
|
||||||
// Test checksum
|
// Test checksum
|
||||||
byte[] actualChecksum = generateChecksum(data);
|
byte[] actualChecksum = generateChecksum(dataSlice);
|
||||||
if (!Arrays.equals(expectedChecksum, actualChecksum))
|
if (!Arrays.equals(expectedChecksum, actualChecksum))
|
||||||
throw new MessageException("Message checksum incorrect");
|
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);
|
return messageType.fromByteBuffer(id, dataSlice);
|
||||||
|
|
||||||
// We successfully read a message, so bump byteBuffer's position to reflect this
|
|
||||||
byteBuffer.position(readBuffer.position());
|
|
||||||
|
|
||||||
return message;
|
|
||||||
} catch (BufferUnderflowException e) {
|
} catch (BufferUnderflowException e) {
|
||||||
// Not enough bytes to fully decode message...
|
// Not enough bytes to fully decode message...
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
|
||||||
byteBuffer.compact();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +233,10 @@ public abstract class Message {
|
|||||||
return Arrays.copyOfRange(Crypto.digest(data), 0, CHECKSUM_LENGTH);
|
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 {
|
public byte[] toBytes() throws MessageException {
|
||||||
try {
|
try {
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
|
||||||
|
@ -87,6 +87,21 @@ public interface ATRepository {
|
|||||||
*/
|
*/
|
||||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException;
|
public List<ATStateData> 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.
|
||||||
|
* <p>
|
||||||
|
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
|
||||||
|
*/
|
||||||
|
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.
|
* Save ATStateData into repository.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -143,13 +143,21 @@ public interface BlockRepository {
|
|||||||
*/
|
*/
|
||||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
|
public List<BlockInfo> 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.
|
||||||
|
* <p>
|
||||||
|
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
|
||||||
|
*/
|
||||||
|
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
|
* @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.
|
* Returns first (lowest height) block that doesn't link back to specified block.
|
||||||
|
@ -47,6 +47,12 @@ public interface Repository extends AutoCloseable {
|
|||||||
|
|
||||||
public void backup(boolean quick) throws DataException;
|
public void backup(boolean quick) throws DataException;
|
||||||
|
|
||||||
|
public void checkpoint(boolean quick) throws DataException;
|
||||||
|
|
||||||
public void performPeriodicMaintenance() throws DataException;
|
public void performPeriodicMaintenance() throws DataException;
|
||||||
|
|
||||||
|
public void exportNodeLocalData() throws DataException;
|
||||||
|
|
||||||
|
public void importDataFromFile(String filename) throws DataException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package org.qortal.repository;
|
|||||||
|
|
||||||
public interface RepositoryFactory {
|
public interface RepositoryFactory {
|
||||||
|
|
||||||
|
public boolean wasPristineAtOpen();
|
||||||
|
|
||||||
public RepositoryFactory reopen() throws DataException;
|
public RepositoryFactory reopen() throws DataException;
|
||||||
|
|
||||||
public Repository getRepository() throws DataException;
|
public Repository getRepository() throws DataException;
|
||||||
|
@ -8,6 +8,13 @@ public abstract class RepositoryManager {
|
|||||||
repositoryFactory = newRepositoryFactory;
|
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 {
|
public static Repository getRepository() throws DataException {
|
||||||
if (repositoryFactory == null)
|
if (repositoryFactory == null)
|
||||||
throw new DataException("No repository available");
|
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 {
|
public static void rebuild() throws DataException {
|
||||||
RepositoryFactory oldRepositoryFactory = repositoryFactory;
|
RepositoryFactory oldRepositoryFactory = repositoryFactory;
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
boolean hadFatalError = resultSet.getBoolean(10);
|
boolean hadFatalError = resultSet.getBoolean(10);
|
||||||
boolean isFrozen = resultSet.getBoolean(11);
|
boolean isFrozen = resultSet.getBoolean(11);
|
||||||
|
|
||||||
Long frozenBalance = resultSet.getLong(11);
|
Long frozenBalance = resultSet.getLong(12);
|
||||||
if (frozenBalance == 0 && resultSet.wasNull())
|
if (frozenBalance == 0 && resultSet.wasNull())
|
||||||
frozenBalance = null;
|
frozenBalance = null;
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
boolean hadFatalError = resultSet.getBoolean(10);
|
boolean hadFatalError = resultSet.getBoolean(10);
|
||||||
boolean isFrozen = resultSet.getBoolean(11);
|
boolean isFrozen = resultSet.getBoolean(11);
|
||||||
|
|
||||||
Long frozenBalance = resultSet.getLong(11);
|
Long frozenBalance = resultSet.getLong(12);
|
||||||
if (frozenBalance == 0 && resultSet.wasNull())
|
if (frozenBalance == 0 && resultSet.wasNull())
|
||||||
frozenBalance = null;
|
frozenBalance = null;
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
bindParams.add(codeHash);
|
bindParams.add(codeHash);
|
||||||
|
|
||||||
if (isExecutable != null) {
|
if (isExecutable != null) {
|
||||||
sql.append("AND is_finished = ? ");
|
sql.append("AND is_finished != ? ");
|
||||||
bindParams.add(isExecutable);
|
bindParams.add(isExecutable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,22 +248,22 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
|
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 "
|
+ "FROM ATStates "
|
||||||
+ "WHERE AT_address = ? AND height = ? "
|
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
|
||||||
|
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
|
||||||
+ "LIMIT 1";
|
+ "LIMIT 1";
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
long created = resultSet.getLong(1);
|
byte[] stateData = resultSet.getBytes(1); // Actually BLOB
|
||||||
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
|
byte[] stateHash = resultSet.getBytes(2);
|
||||||
byte[] stateHash = resultSet.getBytes(3);
|
long fees = resultSet.getLong(3);
|
||||||
long fees = resultSet.getLong(4);
|
boolean isInitial = resultSet.getBoolean(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) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to fetch AT state from repository", e);
|
throw new DataException("Unable to fetch AT state from repository", e);
|
||||||
}
|
}
|
||||||
@ -271,12 +271,13 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ATStateData getLatestATState(String atAddress) throws DataException {
|
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 "
|
+ "FROM ATStates "
|
||||||
+ "WHERE AT_address = ? "
|
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||||
// AT_address then height so the compound primary key is used as an index
|
+ "WHERE ATStates.AT_address = ? "
|
||||||
// Both must be the same direction also
|
// Order by AT_address and height to use compound primary key as index
|
||||||
+ "ORDER BY AT_address DESC, height DESC "
|
// Both must be the same direction (DESC) also
|
||||||
|
+ "ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||||
+ "LIMIT 1 ";
|
+ "LIMIT 1 ";
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
||||||
@ -284,13 +285,12 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
int height = resultSet.getInt(1);
|
int height = resultSet.getInt(1);
|
||||||
long created = resultSet.getLong(2);
|
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
|
||||||
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
|
byte[] stateHash = resultSet.getBytes(3);
|
||||||
byte[] stateHash = resultSet.getBytes(4);
|
long fees = resultSet.getLong(4);
|
||||||
long fees = resultSet.getLong(5);
|
boolean isInitial = resultSet.getBoolean(5);
|
||||||
boolean isInitial = resultSet.getBoolean(6);
|
|
||||||
|
|
||||||
return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to fetch latest AT state from repository", 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);
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
List<Object> bindParams = new ArrayList<>();
|
List<Object> bindParams = new ArrayList<>();
|
||||||
|
|
||||||
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
|
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
|
||||||
+ "FROM ATs "
|
+ "FROM ATs "
|
||||||
+ "CROSS JOIN LATERAL("
|
+ "CROSS JOIN LATERAL("
|
||||||
+ "SELECT height, created_when, state_data, state_hash, fees, is_initial "
|
+ "SELECT height, state_data, state_hash, fees, is_initial "
|
||||||
+ "FROM ATStates "
|
+ "FROM ATStates "
|
||||||
|
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||||
+ "WHERE ATStates.AT_address = ATs.AT_address ");
|
+ "WHERE ATStates.AT_address = ATs.AT_address ");
|
||||||
|
|
||||||
if (minimumFinalHeight != null) {
|
if (minimumFinalHeight != null) {
|
||||||
sql.append("AND height >= ? ");
|
sql.append("AND ATStates.height >= ? ");
|
||||||
bindParams.add(minimumFinalHeight);
|
bindParams.add(minimumFinalHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AT_address then height so the compound primary key is used as an index
|
// Order by AT_address and height to use compound primary key as index
|
||||||
// Both must be the same direction also
|
// Both must be the same direction (DESC) also
|
||||||
sql.append("ORDER BY AT_address DESC, height DESC "
|
sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||||
+ "LIMIT 1 "
|
+ "LIMIT 1 "
|
||||||
+ ") AS FinalATStates "
|
+ ") AS FinalATStates "
|
||||||
+ "WHERE code_hash = ? ");
|
+ "WHERE code_hash = ? ");
|
||||||
@ -339,7 +340,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
bindParams.add(rawExpectedValue);
|
bindParams.add(rawExpectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append(" ORDER BY height ");
|
sql.append(" ORDER BY FinalATStates.height ");
|
||||||
if (reverse != null && reverse)
|
if (reverse != null && reverse)
|
||||||
sql.append("DESC");
|
sql.append("DESC");
|
||||||
|
|
||||||
@ -354,13 +355,12 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
do {
|
do {
|
||||||
String atAddress = resultSet.getString(1);
|
String atAddress = resultSet.getString(1);
|
||||||
int height = resultSet.getInt(2);
|
int height = resultSet.getInt(2);
|
||||||
long created = resultSet.getLong(3);
|
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
|
||||||
byte[] stateData = resultSet.getBytes(4); // Actually BLOB
|
byte[] stateHash = resultSet.getBytes(4);
|
||||||
byte[] stateHash = resultSet.getBytes(5);
|
long fees = resultSet.getLong(5);
|
||||||
long fees = resultSet.getLong(6);
|
boolean isInitial = resultSet.getBoolean(6);
|
||||||
boolean isInitial = resultSet.getBoolean(7);
|
|
||||||
|
|
||||||
ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||||
|
|
||||||
atStates.add(atStateData);
|
atStates.add(atStateData);
|
||||||
} while (resultSet.next());
|
} while (resultSet.next());
|
||||||
@ -374,8 +374,10 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
@Override
|
@Override
|
||||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
||||||
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
||||||
+ "FROM ATStates "
|
+ "FROM ATs "
|
||||||
+ "WHERE height = ? "
|
+ "LEFT OUTER JOIN ATStates "
|
||||||
|
+ "ON ATStates.AT_address = ATs.AT_address AND height = ? "
|
||||||
|
+ "WHERE ATStates.AT_address IS NOT NULL "
|
||||||
+ "ORDER BY created_when ASC";
|
+ "ORDER BY created_when ASC";
|
||||||
|
|
||||||
List<ATStateData> atStates = new ArrayList<>();
|
List<ATStateData> atStates = new ArrayList<>();
|
||||||
@ -402,29 +404,131 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(ATStateData atStateData) throws DataException {
|
public int getAtTrimHeight() throws DataException {
|
||||||
// We shouldn't ever save partial ATStateData
|
String sql = "SELECT AT_trim_height FROM DatabaseInfo";
|
||||||
if (atStateData.getCreation() == null || atStateData.getStateHash() == null || atStateData.getHeight() == null)
|
|
||||||
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
|
|
||||||
|
|
||||||
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
|
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
return resultSet.getInt(1);
|
||||||
.bind("created_when", atStateData.getCreation()).bind("state_data", atStateData.getStateData())
|
} catch (SQLException e) {
|
||||||
.bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees())
|
throw new DataException("Unable to fetch AT state trim height from repository", e);
|
||||||
.bind("is_initial", atStateData.isInitial());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAtTrimHeight(int trimHeight) throws DataException {
|
||||||
|
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
|
||||||
|
// that could result in "transaction rollback: serialization failure"
|
||||||
|
synchronized (this.repository.trimHeightsLock) {
|
||||||
|
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saveHelper.execute(this.repository);
|
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||||
|
this.repository.saveChanges();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
repository.examineException(e);
|
||||||
|
throw new DataException("Unable to set AT state trim height in repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepareForAtStateTrimming() throws DataException {
|
||||||
|
// Rebuild cache of latest AT states that we can't trim
|
||||||
|
String deleteSql = "DELETE FROM LatestATStates";
|
||||||
|
try {
|
||||||
|
this.repository.executeCheckedUpdate(deleteSql);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
repository.examineException(e);
|
||||||
|
throw new DataException("Unable to delete temporary latest AT states cache from repository", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
String insertSql = "INSERT INTO LatestATStates ("
|
||||||
|
+ "SELECT AT_address, height FROM ATs "
|
||||||
|
+ "CROSS JOIN LATERAL("
|
||||||
|
+ "SELECT height FROM ATStates "
|
||||||
|
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||||
|
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||||
|
+ ") "
|
||||||
|
+ ")";
|
||||||
|
try {
|
||||||
|
this.repository.executeCheckedUpdate(insertSql);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
repository.examineException(e);
|
||||||
|
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int trimAtStates(int minHeight, int maxHeight, int limit) throws DataException {
|
||||||
|
if (minHeight >= maxHeight)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// We're often called so no need to trim all states in one go.
|
||||||
|
// Limit updates to reduce CPU and memory load.
|
||||||
|
String sql = "DELETE FROM ATStatesData "
|
||||||
|
+ "WHERE height BETWEEN ? AND ? "
|
||||||
|
+ "AND NOT EXISTS("
|
||||||
|
+ "SELECT TRUE FROM LatestATStates "
|
||||||
|
+ "WHERE LatestATStates.AT_address = ATStatesData.AT_address "
|
||||||
|
+ "AND LatestATStates.height = ATStatesData.height"
|
||||||
|
+ ") "
|
||||||
|
+ "LIMIT ?";
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight, limit);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
repository.examineException(e);
|
||||||
|
throw new DataException("Unable to trim AT states in repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(ATStateData atStateData) throws DataException {
|
||||||
|
// We shouldn't ever save partial ATStateData
|
||||||
|
if (atStateData.getStateHash() == null || atStateData.getHeight() == null)
|
||||||
|
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
|
||||||
|
|
||||||
|
HSQLDBSaver atStatesSaver = new HSQLDBSaver("ATStates");
|
||||||
|
|
||||||
|
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||||
|
.bind("state_hash", atStateData.getStateHash())
|
||||||
|
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
|
||||||
|
|
||||||
|
try {
|
||||||
|
atStatesSaver.execute(this.repository);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to save AT state into repository", e);
|
throw new DataException("Unable to save AT state into repository", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (atStateData.getStateData() != null) {
|
||||||
|
HSQLDBSaver atStatesDataSaver = new HSQLDBSaver("ATStatesData");
|
||||||
|
|
||||||
|
atStatesDataSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||||
|
.bind("state_data", atStateData.getStateData());
|
||||||
|
|
||||||
|
try {
|
||||||
|
atStatesDataSaver.execute(this.repository);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to save AT state data into repository", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?",
|
||||||
|
atStateData.getATAddress(), atStateData.getHeight());
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to delete AT state data from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String atAddress, int height) throws DataException {
|
public void delete(String atAddress, int height) throws DataException {
|
||||||
try {
|
try {
|
||||||
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
|
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
|
||||||
|
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?", atAddress, height);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to delete AT state from repository", e);
|
throw new DataException("Unable to delete AT state from repository", e);
|
||||||
}
|
}
|
||||||
@ -434,6 +538,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
public void deleteATStates(int height) throws DataException {
|
public void deleteATStates(int height) throws DataException {
|
||||||
try {
|
try {
|
||||||
this.repository.delete("ATStates", "height = ?", height);
|
this.repository.delete("ATStates", "height = ?", height);
|
||||||
|
this.repository.delete("ATStatesData", "height = ?", height);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to delete AT states from repository", e);
|
throw new DataException("Unable to delete AT states from repository", e);
|
||||||
}
|
}
|
||||||
|
@ -462,13 +462,46 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
|
public int getOnlineAccountsSignaturesTrimHeight() throws DataException {
|
||||||
// We're often called so no need to trim all blocks in one go.
|
String sql = "SELECT online_signatures_trim_height FROM DatabaseInfo";
|
||||||
// Limit updates to reduce CPU and memory load.
|
|
||||||
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL LIMIT 1440";
|
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return resultSet.getInt(1);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch online accounts signatures trim height from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException {
|
||||||
|
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
|
||||||
|
// that could result in "transaction rollback: serialization failure"
|
||||||
|
synchronized (this.repository.trimHeightsLock) {
|
||||||
|
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return this.repository.executeCheckedUpdate(sql, timestamp);
|
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||||
|
this.repository.saveChanges();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
repository.examineException(e);
|
||||||
|
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int trimOldOnlineAccountsSignatures(int minHeight, int maxHeight) throws DataException {
|
||||||
|
// We're often called so no need to trim all blocks in one go.
|
||||||
|
// Limit updates to reduce CPU and memory load.
|
||||||
|
String sql = "UPDATE Blocks SET online_accounts_signatures = NULL "
|
||||||
|
+ "WHERE online_accounts_signatures IS NOT NULL "
|
||||||
|
+ "AND height BETWEEN ? AND ?";
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
repository.examineException(e);
|
repository.examineException(e);
|
||||||
throw new DataException("Unable to trim old online accounts signatures in repository", e);
|
throw new DataException("Unable to trim old online accounts signatures in repository", e);
|
||||||
|
@ -21,11 +21,16 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
/**
|
/**
|
||||||
* Apply any incremental changes to database schema.
|
* Apply any incremental changes to database schema.
|
||||||
*
|
*
|
||||||
|
* @return true if database was non-existent/empty, false otherwise
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
public static void updateDatabase(Connection connection) throws SQLException {
|
public static boolean updateDatabase(Connection connection) throws SQLException {
|
||||||
while (databaseUpdating(connection))
|
final boolean wasPristine = fetchDatabaseVersion(connection) == 0;
|
||||||
|
|
||||||
|
while (databaseUpdating(connection, wasPristine))
|
||||||
incrementDatabaseVersion(connection);
|
incrementDatabaseVersion(connection);
|
||||||
|
|
||||||
|
return wasPristine;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,23 +48,21 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
/**
|
/**
|
||||||
* Fetch current version of database schema.
|
* Fetch current version of database schema.
|
||||||
*
|
*
|
||||||
* @return int, 0 if no schema yet
|
* @return database version, or 0 if no schema yet
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
private static int fetchDatabaseVersion(Connection connection) throws SQLException {
|
private static int fetchDatabaseVersion(Connection connection) throws SQLException {
|
||||||
int databaseVersion = 0;
|
|
||||||
|
|
||||||
try (Statement stmt = connection.createStatement()) {
|
try (Statement stmt = connection.createStatement()) {
|
||||||
if (stmt.execute("SELECT version FROM DatabaseInfo"))
|
if (stmt.execute("SELECT version FROM DatabaseInfo"))
|
||||||
try (ResultSet resultSet = stmt.getResultSet()) {
|
try (ResultSet resultSet = stmt.getResultSet()) {
|
||||||
if (resultSet.next())
|
if (resultSet.next())
|
||||||
databaseVersion = resultSet.getInt(1);
|
return resultSet.getInt(1);
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
// empty database
|
// empty database
|
||||||
}
|
}
|
||||||
|
|
||||||
return databaseVersion;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,7 +71,7 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
* @return true - if a schema update happened, false otherwise
|
* @return true - if a schema update happened, false otherwise
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
private static boolean databaseUpdating(Connection connection) throws SQLException {
|
private static boolean databaseUpdating(Connection connection, boolean wasPristine) throws SQLException {
|
||||||
int databaseVersion = fetchDatabaseVersion(connection);
|
int databaseVersion = fetchDatabaseVersion(connection);
|
||||||
|
|
||||||
try (Statement stmt = connection.createStatement()) {
|
try (Statement stmt = connection.createStatement()) {
|
||||||
@ -215,6 +218,8 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
+ "PRIMARY KEY (account))");
|
+ "PRIMARY KEY (account))");
|
||||||
// For looking up an account by public key
|
// For looking up an account by public key
|
||||||
stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)");
|
stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)");
|
||||||
|
// Use a separate table space as this table will be very large.
|
||||||
|
stmt.execute("SET TABLE Accounts NEW SPACE");
|
||||||
|
|
||||||
// Account balances
|
// Account balances
|
||||||
stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, "
|
stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, "
|
||||||
@ -223,6 +228,8 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("CREATE INDEX AccountBalancesAssetBalanceIndex ON AccountBalances (asset_id, balance)");
|
stmt.execute("CREATE INDEX AccountBalancesAssetBalanceIndex ON AccountBalances (asset_id, balance)");
|
||||||
// Add CHECK constraint to account balances
|
// Add CHECK constraint to account balances
|
||||||
stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)");
|
stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)");
|
||||||
|
// Use a separate table space as this table will be very large.
|
||||||
|
stmt.execute("SET TABLE AccountBalances NEW SPACE");
|
||||||
|
|
||||||
// Keeping track of QORT gained from holding legacy QORA
|
// Keeping track of QORT gained from holding legacy QORA
|
||||||
stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, "
|
stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, "
|
||||||
@ -420,6 +427,8 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||||
// For finding per-block AT states, ordered by creation timestamp
|
// For finding per-block AT states, ordered by creation timestamp
|
||||||
stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, created_when)");
|
stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, created_when)");
|
||||||
|
// Use a separate table space as this table will be very large.
|
||||||
|
stmt.execute("SET TABLE ATStates NEW SPACE");
|
||||||
|
|
||||||
// Deploy CIYAM AT Transactions
|
// Deploy CIYAM AT Transactions
|
||||||
stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, "
|
stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, "
|
||||||
@ -658,6 +667,115 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 25:
|
case 25:
|
||||||
|
// DISABLED: improved version in case 30!
|
||||||
|
// Remove excess created_when from ATStates
|
||||||
|
// stmt.execute("ALTER TABLE ATStates DROP created_when");
|
||||||
|
// stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 26:
|
||||||
|
// Support for trimming
|
||||||
|
stmt.execute("ALTER TABLE DatabaseInfo ADD AT_trim_height INT NOT NULL DEFAULT 0");
|
||||||
|
stmt.execute("ALTER TABLE DatabaseInfo ADD online_signatures_trim_height INT NOT NULL DEFAULT 0");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 27:
|
||||||
|
// More indexes
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS PaymentTransactionsRecipientIndex ON PaymentTransactions (recipient)");
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS ATTransactionsRecipientIndex ON ATTransactions (recipient)");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 28:
|
||||||
|
// Latest AT state cache
|
||||||
|
stmt.execute("CREATE TEMPORARY TABLE IF NOT EXISTS LatestATStates ("
|
||||||
|
+ "AT_address QortalAddress NOT NULL, "
|
||||||
|
+ "height INT NOT NULL"
|
||||||
|
+ ")");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 29:
|
||||||
|
// Turn off HSQLDB redo-log "blockchain.log" and periodically call "CHECKPOINT" ourselves
|
||||||
|
stmt.execute("SET FILES LOG FALSE");
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 30:
|
||||||
|
// Split AT state data off to new table for better performance/management.
|
||||||
|
|
||||||
|
if (!wasPristine && !"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) {
|
||||||
|
// First, backup node-local data in case user wants to avoid long reshape and use bootstrap instead
|
||||||
|
try (ResultSet resultSet = stmt.executeQuery("SELECT COUNT(*) FROM MintingAccounts")) {
|
||||||
|
int rowCount = resultSet.next() ? resultSet.getInt(1) : 0;
|
||||||
|
if (rowCount > 0) {
|
||||||
|
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
|
||||||
|
LOGGER.info("Exported sensitive/node-local minting keys into MintingAccounts.script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ResultSet resultSet = stmt.executeQuery("SELECT COUNT(*) FROM TradeBotStates")) {
|
||||||
|
int rowCount = resultSet.next() ? resultSet.getInt(1) : 0;
|
||||||
|
if (rowCount > 0) {
|
||||||
|
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
|
||||||
|
LOGGER.info("Exported sensitive/node-local trade-bot states into TradeBotStates.script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("If following reshape takes too long, use bootstrap and import node-local data using API's POST /admin/repository/data");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new AT-states table without full state data
|
||||||
|
stmt.execute("CREATE TABLE ATStatesNew ("
|
||||||
|
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
|
||||||
|
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, "
|
||||||
|
+ "PRIMARY KEY (AT_address, height), "
|
||||||
|
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||||
|
stmt.execute("SET TABLE ATStatesNew NEW SPACE");
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
|
|
||||||
|
ResultSet resultSet = stmt.executeQuery("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1");
|
||||||
|
final int blockchainHeight = resultSet.next() ? resultSet.getInt(1) : 0;
|
||||||
|
final int heightStep = 100;
|
||||||
|
|
||||||
|
LOGGER.info("Rebuilding AT state summaries in repository - this might take a while... (approx. 2 mins on high-spec)");
|
||||||
|
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
|
||||||
|
stmt.execute("INSERT INTO ATStatesNew ("
|
||||||
|
+ "SELECT AT_address, height, state_hash, fees, is_initial "
|
||||||
|
+ "FROM ATStates "
|
||||||
|
+ "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
|
||||||
|
+ ")");
|
||||||
|
stmt.execute("COMMIT");
|
||||||
|
}
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
|
|
||||||
|
LOGGER.info("Rebuilding AT states height index in repository - this might take about 3x longer...");
|
||||||
|
stmt.execute("CREATE INDEX ATStatesHeightIndex ON ATStatesNew (height)");
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
|
|
||||||
|
stmt.execute("CREATE TABLE ATStatesData ("
|
||||||
|
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_data ATState NOT NULL, "
|
||||||
|
+ "PRIMARY KEY (height, AT_address), "
|
||||||
|
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||||
|
stmt.execute("SET TABLE ATStatesData NEW SPACE");
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
|
|
||||||
|
LOGGER.info("Rebuilding AT state data in repository - this might take a while... (approx. 2 mins on high-spec)");
|
||||||
|
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
|
||||||
|
stmt.execute("INSERT INTO ATStatesData ("
|
||||||
|
+ "SELECT AT_address, height, state_data "
|
||||||
|
+ "FROM ATstates "
|
||||||
|
+ "WHERE state_data IS NOT NULL "
|
||||||
|
+ "AND height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
|
||||||
|
+ ")");
|
||||||
|
stmt.execute("COMMIT");
|
||||||
|
}
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
|
|
||||||
|
stmt.execute("DROP TABLE ATStates");
|
||||||
|
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 31:
|
||||||
// Multiple blockchains, ACCTs and trade-bots
|
// Multiple blockchains, ACCTs and trade-bots
|
||||||
stmt.execute("ALTER TABLE TradeBotStates ADD COLUMN acct_name VARCHAR(40) BEFORE trade_state");
|
stmt.execute("ALTER TABLE TradeBotStates ADD COLUMN acct_name VARCHAR(40) BEFORE trade_state");
|
||||||
stmt.execute("UPDATE TradeBotStates SET acct_name = 'BitcoinACCTv1' WHERE acct_name IS NULL");
|
stmt.execute("UPDATE TradeBotStates SET acct_name = 'BitcoinACCTv1' WHERE acct_name IS NULL");
|
||||||
|
@ -24,6 +24,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -53,12 +54,27 @@ public class HSQLDBRepository implements Repository {
|
|||||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
|
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
|
||||||
|
|
||||||
protected Connection connection;
|
protected Connection connection;
|
||||||
protected Deque<Savepoint> savepoints = new ArrayDeque<>(3);
|
protected final Deque<Savepoint> savepoints = new ArrayDeque<>(3);
|
||||||
protected boolean debugState = false;
|
protected boolean debugState = false;
|
||||||
protected Long slowQueryThreshold = null;
|
protected Long slowQueryThreshold = null;
|
||||||
protected List<String> sqlStatements;
|
protected List<String> sqlStatements;
|
||||||
protected long sessionId;
|
protected long sessionId;
|
||||||
protected Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
|
protected final Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
|
||||||
|
protected final Object trimHeightsLock = new Object();
|
||||||
|
|
||||||
|
private final ATRepository atRepository = new HSQLDBATRepository(this);
|
||||||
|
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
|
||||||
|
private final ArbitraryRepository arbitraryRepository = new HSQLDBArbitraryRepository(this);
|
||||||
|
private final AssetRepository assetRepository = new HSQLDBAssetRepository(this);
|
||||||
|
private final BlockRepository blockRepository = new HSQLDBBlockRepository(this);
|
||||||
|
private final ChatRepository chatRepository = new HSQLDBChatRepository(this);
|
||||||
|
private final CrossChainRepository crossChainRepository = new HSQLDBCrossChainRepository(this);
|
||||||
|
private final GroupRepository groupRepository = new HSQLDBGroupRepository(this);
|
||||||
|
private final MessageRepository messageRepository = new HSQLDBMessageRepository(this);
|
||||||
|
private final NameRepository nameRepository = new HSQLDBNameRepository(this);
|
||||||
|
private final NetworkRepository networkRepository = new HSQLDBNetworkRepository(this);
|
||||||
|
private final TransactionRepository transactionRepository = new HSQLDBTransactionRepository(this);
|
||||||
|
private final VotingRepository votingRepository = new HSQLDBVotingRepository(this);
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
@ -92,67 +108,67 @@ public class HSQLDBRepository implements Repository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ATRepository getATRepository() {
|
public ATRepository getATRepository() {
|
||||||
return new HSQLDBATRepository(this);
|
return this.atRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccountRepository getAccountRepository() {
|
public AccountRepository getAccountRepository() {
|
||||||
return new HSQLDBAccountRepository(this);
|
return this.accountRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ArbitraryRepository getArbitraryRepository() {
|
public ArbitraryRepository getArbitraryRepository() {
|
||||||
return new HSQLDBArbitraryRepository(this);
|
return this.arbitraryRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AssetRepository getAssetRepository() {
|
public AssetRepository getAssetRepository() {
|
||||||
return new HSQLDBAssetRepository(this);
|
return this.assetRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BlockRepository getBlockRepository() {
|
public BlockRepository getBlockRepository() {
|
||||||
return new HSQLDBBlockRepository(this);
|
return this.blockRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChatRepository getChatRepository() {
|
public ChatRepository getChatRepository() {
|
||||||
return new HSQLDBChatRepository(this);
|
return this.chatRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CrossChainRepository getCrossChainRepository() {
|
public CrossChainRepository getCrossChainRepository() {
|
||||||
return new HSQLDBCrossChainRepository(this);
|
return this.crossChainRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupRepository getGroupRepository() {
|
public GroupRepository getGroupRepository() {
|
||||||
return new HSQLDBGroupRepository(this);
|
return this.groupRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MessageRepository getMessageRepository() {
|
public MessageRepository getMessageRepository() {
|
||||||
return new HSQLDBMessageRepository(this);
|
return this.messageRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NameRepository getNameRepository() {
|
public NameRepository getNameRepository() {
|
||||||
return new HSQLDBNameRepository(this);
|
return this.nameRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NetworkRepository getNetworkRepository() {
|
public NetworkRepository getNetworkRepository() {
|
||||||
return new HSQLDBNetworkRepository(this);
|
return this.networkRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TransactionRepository getTransactionRepository() {
|
public TransactionRepository getTransactionRepository() {
|
||||||
return new HSQLDBTransactionRepository(this);
|
return this.transactionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VotingRepository getVotingRepository() {
|
public VotingRepository getVotingRepository() {
|
||||||
return new HSQLDBVotingRepository(this);
|
return this.votingRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -169,8 +185,20 @@ public class HSQLDBRepository implements Repository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveChanges() throws DataException {
|
public void saveChanges() throws DataException {
|
||||||
|
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.connection.commit();
|
this.connection.commit();
|
||||||
|
|
||||||
|
if (this.slowQueryThreshold != null) {
|
||||||
|
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||||
|
|
||||||
|
if (queryTime > this.slowQueryThreshold) {
|
||||||
|
LOGGER.info(() -> String.format("[Session %d] HSQLDB COMMIT took %d ms", this.sessionId, queryTime), new SQLException("slow commit"));
|
||||||
|
|
||||||
|
logStatements();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("commit error", e);
|
throw new DataException("commit error", e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -194,7 +222,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
this.savepoints.clear();
|
this.savepoints.clear();
|
||||||
|
|
||||||
// Before clearing statements so we can log what led to assertion error
|
// Before clearing statements so we can log what led to assertion error
|
||||||
assertEmptyTransaction("transaction commit");
|
assertEmptyTransaction("transaction rollback");
|
||||||
|
|
||||||
if (this.sqlStatements != null)
|
if (this.sqlStatements != null)
|
||||||
this.sqlStatements.clear();
|
this.sqlStatements.clear();
|
||||||
@ -284,12 +312,13 @@ public class HSQLDBRepository implements Repository {
|
|||||||
Path oldRepoDirPath = Paths.get(dbPathname).getParent();
|
Path oldRepoDirPath = Paths.get(dbPathname).getParent();
|
||||||
|
|
||||||
// Delete old repository files
|
// Delete old repository files
|
||||||
Files.walk(oldRepoDirPath)
|
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||||
.sorted(Comparator.reverseOrder())
|
paths.sorted(Comparator.reverseOrder())
|
||||||
.map(Path::toFile)
|
.map(Path::toFile)
|
||||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||||
.forEach(File::delete);
|
.forEach(File::delete);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (NoSuchFileException e) {
|
} catch (NoSuchFileException e) {
|
||||||
// Nothing to remove
|
// Nothing to remove
|
||||||
} catch (SQLException | IOException e) {
|
} catch (SQLException | IOException e) {
|
||||||
@ -328,11 +357,12 @@ public class HSQLDBRepository implements Repository {
|
|||||||
Path backupDirPath = Paths.get(backupPathname).getParent();
|
Path backupDirPath = Paths.get(backupPathname).getParent();
|
||||||
String backupDirPathname = backupDirPath.toString();
|
String backupDirPathname = backupDirPath.toString();
|
||||||
|
|
||||||
Files.walk(backupDirPath)
|
try (Stream<Path> paths = Files.walk(backupDirPath)) {
|
||||||
.sorted(Comparator.reverseOrder())
|
paths.sorted(Comparator.reverseOrder())
|
||||||
.map(Path::toFile)
|
.map(Path::toFile)
|
||||||
.filter(file -> file.getPath().startsWith(backupDirPathname))
|
.filter(file -> file.getPath().startsWith(backupDirPathname))
|
||||||
.forEach(File::delete);
|
.forEach(File::delete);
|
||||||
|
}
|
||||||
} catch (NoSuchFileException e) {
|
} catch (NoSuchFileException e) {
|
||||||
// Nothing to remove
|
// Nothing to remove
|
||||||
} catch (SQLException | IOException e) {
|
} catch (SQLException | IOException e) {
|
||||||
@ -347,18 +377,56 @@ public class HSQLDBRepository implements Repository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkpoint(boolean quick) throws DataException {
|
||||||
|
try (Statement stmt = this.connection.createStatement()) {
|
||||||
|
stmt.execute(quick ? "CHECKPOINT" : "CHECKPOINT DEFRAG");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to perform repository checkpoint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void performPeriodicMaintenance() throws DataException {
|
public void performPeriodicMaintenance() throws DataException {
|
||||||
// Defrag DB - takes a while!
|
// Defrag DB - takes a while!
|
||||||
try (Statement stmt = this.connection.createStatement()) {
|
try (Statement stmt = this.connection.createStatement()) {
|
||||||
|
LOGGER.info("performing maintenance - this will take a while");
|
||||||
|
stmt.execute("CHECKPOINT");
|
||||||
stmt.execute("CHECKPOINT DEFRAG");
|
stmt.execute("CHECKPOINT DEFRAG");
|
||||||
|
LOGGER.info("maintenance completed");
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to defrag repository");
|
throw new DataException("Unable to defrag repository");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exportNodeLocalData() throws DataException {
|
||||||
|
try (Statement stmt = this.connection.createStatement()) {
|
||||||
|
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
|
||||||
|
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
|
||||||
|
LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to export sensitive/node-local data from repository");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void importDataFromFile(String filename) throws DataException {
|
||||||
|
try (Statement stmt = this.connection.createStatement()) {
|
||||||
|
LOGGER.info(() -> String.format("Importing data into repository from %s", filename));
|
||||||
|
|
||||||
|
String escapedFilename = stmt.enquoteLiteral(filename);
|
||||||
|
stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " STOP ON ERROR");
|
||||||
|
|
||||||
|
LOGGER.info(() -> String.format("Imported data into repository from %s", filename));
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.info(() -> String.format("Failed to import data into repository from %s: %s", filename, e.getMessage()));
|
||||||
|
throw new DataException("Unable to export sensitive/node-local data from repository: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns DB pathname from passed connection URL. If memory DB, returns "mem". */
|
/** Returns DB pathname from passed connection URL. If memory DB, returns "mem". */
|
||||||
private static String getDbPathname(String connectionUrl) {
|
/*package*/ static String getDbPathname(String connectionUrl) {
|
||||||
Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)");
|
Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)");
|
||||||
Matcher matcher = pattern.matcher(connectionUrl);
|
Matcher matcher = pattern.matcher(connectionUrl);
|
||||||
|
|
||||||
@ -394,11 +462,12 @@ public class HSQLDBRepository implements Repository {
|
|||||||
LOGGER.info("Attempting repository recovery using backup");
|
LOGGER.info("Attempting repository recovery using backup");
|
||||||
|
|
||||||
// Move old repository files out the way
|
// Move old repository files out the way
|
||||||
Files.walk(oldRepoDirPath)
|
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||||
.sorted(Comparator.reverseOrder())
|
paths.sorted(Comparator.reverseOrder())
|
||||||
.map(Path::toFile)
|
.map(Path::toFile)
|
||||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||||
.forEach(File::delete);
|
.forEach(File::delete);
|
||||||
|
}
|
||||||
|
|
||||||
try (Statement stmt = connection.createStatement()) {
|
try (Statement stmt = connection.createStatement()) {
|
||||||
// Now "backup" the backup back to original repository location (the parent).
|
// Now "backup" the backup back to original repository location (the parent).
|
||||||
@ -438,6 +507,10 @@ public class HSQLDBRepository implements Repository {
|
|||||||
if (this.sqlStatements != null)
|
if (this.sqlStatements != null)
|
||||||
this.sqlStatements.add(sql);
|
this.sqlStatements.add(sql);
|
||||||
|
|
||||||
|
return cachePreparedStatement(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreparedStatement cachePreparedStatement(String sql) throws SQLException {
|
||||||
/*
|
/*
|
||||||
* We cache a duplicate PreparedStatement for this SQL string,
|
* We cache a duplicate PreparedStatement for this SQL string,
|
||||||
* which we never close, which means HSQLDB also caches a parsed,
|
* which we never close, which means HSQLDB also caches a parsed,
|
||||||
@ -446,10 +519,21 @@ public class HSQLDBRepository implements Repository {
|
|||||||
*
|
*
|
||||||
* See org.hsqldb.StatementManager for more details.
|
* See org.hsqldb.StatementManager for more details.
|
||||||
*/
|
*/
|
||||||
if (!this.preparedStatementCache.containsKey(sql))
|
PreparedStatement preparedStatement = this.preparedStatementCache.get(sql);
|
||||||
this.preparedStatementCache.put(sql, this.connection.prepareStatement(sql));
|
if (preparedStatement == null || preparedStatement.isClosed()) {
|
||||||
|
if (preparedStatement != null)
|
||||||
|
// This shouldn't occur, so log, but recompile
|
||||||
|
LOGGER.debug(() -> String.format("Recompiling closed PreparedStatement: %s", sql));
|
||||||
|
|
||||||
return this.connection.prepareStatement(sql);
|
preparedStatement = this.connection.prepareStatement(sql);
|
||||||
|
this.preparedStatementCache.put(sql, preparedStatement);
|
||||||
|
} else {
|
||||||
|
// Clean up ready for reuse
|
||||||
|
preparedStatement.clearBatch();
|
||||||
|
preparedStatement.clearParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
return preparedStatement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -465,9 +549,8 @@ public class HSQLDBRepository implements Repository {
|
|||||||
public ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
|
public ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
|
||||||
PreparedStatement preparedStatement = this.prepareStatement(sql);
|
PreparedStatement preparedStatement = this.prepareStatement(sql);
|
||||||
|
|
||||||
// Close the PreparedStatement when the ResultSet is closed otherwise there's a potential resource leak.
|
// We don't close the PreparedStatement when the ResultSet is closed because we cached PreparedStatements now.
|
||||||
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
|
// They are cleaned up when connection/session is closed.
|
||||||
preparedStatement.closeOnCompletion();
|
|
||||||
|
|
||||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||||
|
|
||||||
@ -477,7 +560,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||||
|
|
||||||
if (queryTime > this.slowQueryThreshold) {
|
if (queryTime > this.slowQueryThreshold) {
|
||||||
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
|
||||||
|
|
||||||
logStatements();
|
logStatements();
|
||||||
}
|
}
|
||||||
@ -556,7 +639,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
if (batchedObjects == null || batchedObjects.isEmpty())
|
if (batchedObjects == null || batchedObjects.isEmpty())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
|
PreparedStatement preparedStatement = this.prepareStatement(sql);
|
||||||
for (Object[] objects : batchedObjects) {
|
for (Object[] objects : batchedObjects) {
|
||||||
this.bindStatementParams(preparedStatement, objects);
|
this.bindStatementParams(preparedStatement, objects);
|
||||||
preparedStatement.addBatch();
|
preparedStatement.addBatch();
|
||||||
@ -570,7 +653,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||||
|
|
||||||
if (queryTime > this.slowQueryThreshold) {
|
if (queryTime > this.slowQueryThreshold) {
|
||||||
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
|
||||||
|
|
||||||
logStatements();
|
logStatements();
|
||||||
}
|
}
|
||||||
@ -586,7 +669,6 @@ public class HSQLDBRepository implements Repository {
|
|||||||
|
|
||||||
return totalCount;
|
return totalCount;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch last value of IDENTITY column after an INSERT statement.
|
* Fetch last value of IDENTITY column after an INSERT statement.
|
||||||
@ -765,15 +847,15 @@ public class HSQLDBRepository implements Repository {
|
|||||||
if (this.sqlStatements == null)
|
if (this.sqlStatements == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LOGGER.info(() -> String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
|
LOGGER.info(() -> String.format("[Session %d] HSQLDB SQL statements leading up to this were:", this.sessionId));
|
||||||
|
|
||||||
for (String sql : this.sqlStatements)
|
for (String sql : this.sqlStatements)
|
||||||
LOGGER.info(sql);
|
LOGGER.info(() -> String.format("[Session %d] %s", this.sessionId, sql));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Logs other HSQLDB sessions then returns passed exception */
|
/** Logs other HSQLDB sessions then returns passed exception */
|
||||||
public SQLException examineException(SQLException e) {
|
public SQLException examineException(SQLException e) {
|
||||||
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
|
LOGGER.error(() -> String.format("[Session %d] HSQLDB error: %s", this.sessionId, e.getMessage()), e);
|
||||||
|
|
||||||
logStatements();
|
logStatements();
|
||||||
|
|
||||||
@ -807,14 +889,19 @@ public class HSQLDBRepository implements Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void assertEmptyTransaction(String context) throws DataException {
|
private void assertEmptyTransaction(String context) throws DataException {
|
||||||
try (Statement stmt = this.connection.createStatement()) {
|
String sql = "SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = ?";
|
||||||
|
|
||||||
|
try {
|
||||||
|
PreparedStatement stmt = this.cachePreparedStatement(sql);
|
||||||
|
stmt.setLong(1, this.sessionId);
|
||||||
|
|
||||||
// Diagnostic check for uncommitted changes
|
// Diagnostic check for uncommitted changes
|
||||||
if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = " + this.sessionId)) // TRANSACTION_SIZE() broken?
|
if (!stmt.execute()) // TRANSACTION_SIZE() broken?
|
||||||
throw new DataException("Unable to check repository status after " + context);
|
throw new DataException("Unable to check repository status after " + context);
|
||||||
|
|
||||||
try (ResultSet resultSet = stmt.getResultSet()) {
|
try (ResultSet resultSet = stmt.getResultSet()) {
|
||||||
if (resultSet == null || !resultSet.next()) {
|
if (resultSet == null || !resultSet.next()) {
|
||||||
LOGGER.warn(String.format("Unable to check repository status after %s", context));
|
LOGGER.warn(() -> String.format("Unable to check repository status after %s", context));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -822,7 +909,11 @@ public class HSQLDBRepository implements Repository {
|
|||||||
int transactionCount = resultSet.getInt(2);
|
int transactionCount = resultSet.getInt(2);
|
||||||
|
|
||||||
if (inTransaction && transactionCount != 0) {
|
if (inTransaction && transactionCount != 0) {
|
||||||
LOGGER.warn(String.format("Uncommitted changes (%d) after %s, session [%d]", transactionCount, context, this.sessionId), new Exception("Uncommitted repository changes"));
|
LOGGER.warn(() -> String.format("Uncommitted changes (%d) after %s, session [%d]",
|
||||||
|
transactionCount,
|
||||||
|
context,
|
||||||
|
this.sessionId),
|
||||||
|
new Exception("Uncommitted repository changes"));
|
||||||
logStatements();
|
logStatements();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
|||||||
|
|
||||||
private String connectionUrl;
|
private String connectionUrl;
|
||||||
private HSQLDBPool connectionPool;
|
private HSQLDBPool connectionPool;
|
||||||
|
private final boolean wasPristine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt>.
|
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt>.
|
||||||
@ -65,12 +66,17 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
|||||||
|
|
||||||
// Perform DB updates?
|
// Perform DB updates?
|
||||||
try (final Connection connection = this.connectionPool.getConnection()) {
|
try (final Connection connection = this.connectionPool.getConnection()) {
|
||||||
HSQLDBDatabaseUpdates.updateDatabase(connection);
|
this.wasPristine = HSQLDBDatabaseUpdates.updateDatabase(connection);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Repository initialization error", e);
|
throw new DataException("Repository initialization error", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wasPristineAtOpen() {
|
||||||
|
return this.wasPristine;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepositoryFactory reopen() throws DataException {
|
public RepositoryFactory reopen() throws DataException {
|
||||||
return new HSQLDBRepositoryFactory(this.connectionUrl);
|
return new HSQLDBRepositoryFactory(this.connectionUrl);
|
||||||
|
@ -60,7 +60,9 @@ public class HSQLDBSaver {
|
|||||||
*/
|
*/
|
||||||
public boolean execute(HSQLDBRepository repository) throws SQLException {
|
public boolean execute(HSQLDBRepository repository) throws SQLException {
|
||||||
String sql = this.formatInsertWithPlaceholders();
|
String sql = this.formatInsertWithPlaceholders();
|
||||||
try (PreparedStatement preparedStatement = repository.prepareStatement(sql)) {
|
|
||||||
|
try {
|
||||||
|
PreparedStatement preparedStatement = repository.prepareStatement(sql);
|
||||||
this.bindValues(preparedStatement);
|
this.bindValues(preparedStatement);
|
||||||
|
|
||||||
return preparedStatement.execute();
|
return preparedStatement.execute();
|
||||||
|
@ -5,6 +5,7 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.xml.bind.JAXBContext;
|
import javax.xml.bind.JAXBContext;
|
||||||
import javax.xml.bind.JAXBException;
|
import javax.xml.bind.JAXBException;
|
||||||
@ -42,6 +43,9 @@ public class Settings {
|
|||||||
// Settings, and other config files
|
// Settings, and other config files
|
||||||
private String userPath;
|
private String userPath;
|
||||||
|
|
||||||
|
// General
|
||||||
|
private String localeLang = Locale.getDefault().getLanguage();
|
||||||
|
|
||||||
// Common to all networking (API/P2P)
|
// Common to all networking (API/P2P)
|
||||||
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
|
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
|
||||||
|
|
||||||
@ -62,6 +66,7 @@ public class Settings {
|
|||||||
"::1", "127.0.0.1"
|
"::1", "127.0.0.1"
|
||||||
};
|
};
|
||||||
private Boolean apiRestricted;
|
private Boolean apiRestricted;
|
||||||
|
private String apiKey = null;
|
||||||
private boolean apiLoggingEnabled = false;
|
private boolean apiLoggingEnabled = false;
|
||||||
private boolean apiDocumentationEnabled = false;
|
private boolean apiDocumentationEnabled = false;
|
||||||
// Both of these need to be set for API to use SSL
|
// Both of these need to be set for API to use SSL
|
||||||
@ -80,6 +85,26 @@ public class Settings {
|
|||||||
private long repositoryBackupInterval = 0; // ms
|
private long repositoryBackupInterval = 0; // ms
|
||||||
/** Whether to show a notification when we backup repository. */
|
/** Whether to show a notification when we backup repository. */
|
||||||
private boolean showBackupNotification = false;
|
private boolean showBackupNotification = false;
|
||||||
|
/** How long between repository checkpoints (ms). */
|
||||||
|
private long repositoryCheckpointInterval = 60 * 60 * 1000L; // 1 hour (ms) default
|
||||||
|
/** Whether to show a notification when we perform repository 'checkpoint'. */
|
||||||
|
private boolean showCheckpointNotification = false;
|
||||||
|
|
||||||
|
/** How long to keep old, full, AT state data (ms). */
|
||||||
|
private long atStatesMaxLifetime = 2 * 7 * 24 * 60 * 60 * 1000L; // milliseconds
|
||||||
|
/** How often to attempt AT state trimming (ms). */
|
||||||
|
private long atStatesTrimInterval = 5678L; // milliseconds
|
||||||
|
/** Block height range to scan for trimmable AT states.<br>
|
||||||
|
* This has a significant effect on execution time. */
|
||||||
|
private int atStatesTrimBatchSize = 100; // blocks
|
||||||
|
/** Max number of AT states to trim in one go. */
|
||||||
|
private int atStatesTrimLimit = 4000; // records
|
||||||
|
|
||||||
|
/** How often to attempt online accounts signatures trimming (ms). */
|
||||||
|
private long onlineSignaturesTrimInterval = 9876L; // milliseconds
|
||||||
|
/** Block height range to scan for trimmable online accounts signatures.<br>
|
||||||
|
* This has a significant effect on execution time. */
|
||||||
|
private int onlineSignaturesTrimBatchSize = 100; // blocks
|
||||||
|
|
||||||
// Peer-to-peer related
|
// Peer-to-peer related
|
||||||
private boolean isTestNet = false;
|
private boolean isTestNet = false;
|
||||||
@ -253,6 +278,9 @@ public class Settings {
|
|||||||
// Validation goes here
|
// Validation goes here
|
||||||
if (this.minBlockchainPeers < 1)
|
if (this.minBlockchainPeers < 1)
|
||||||
throwValidationError("minBlockchainPeers must be at least 1");
|
throwValidationError("minBlockchainPeers must be at least 1");
|
||||||
|
|
||||||
|
if (this.apiKey != null && this.apiKey.trim().length() < 8)
|
||||||
|
throwValidationError("apiKey must be at least 8 characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters / setters
|
// Getters / setters
|
||||||
@ -261,6 +289,10 @@ public class Settings {
|
|||||||
return this.userPath;
|
return this.userPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLocaleLang() {
|
||||||
|
return this.localeLang;
|
||||||
|
}
|
||||||
|
|
||||||
public int getUiServerPort() {
|
public int getUiServerPort() {
|
||||||
return this.uiPort;
|
return this.uiPort;
|
||||||
}
|
}
|
||||||
@ -297,6 +329,10 @@ public class Settings {
|
|||||||
return !BlockChain.getInstance().isTestChain();
|
return !BlockChain.getInstance().isTestChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getApiKey() {
|
||||||
|
return this.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isApiLoggingEnabled() {
|
public boolean isApiLoggingEnabled() {
|
||||||
return this.apiLoggingEnabled;
|
return this.apiLoggingEnabled;
|
||||||
}
|
}
|
||||||
@ -412,4 +448,36 @@ public class Settings {
|
|||||||
return this.showBackupNotification;
|
return this.showBackupNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getRepositoryCheckpointInterval() {
|
||||||
|
return this.repositoryCheckpointInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getShowCheckpointNotification() {
|
||||||
|
return this.showCheckpointNotification;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAtStatesMaxLifetime() {
|
||||||
|
return this.atStatesMaxLifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAtStatesTrimInterval() {
|
||||||
|
return this.atStatesTrimInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAtStatesTrimBatchSize() {
|
||||||
|
return this.atStatesTrimBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAtStatesTrimLimit() {
|
||||||
|
return this.atStatesTrimLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOnlineSignaturesTrimInterval() {
|
||||||
|
return this.onlineSignaturesTrimInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOnlineSignaturesTrimBatchSize() {
|
||||||
|
return this.onlineSignaturesTrimBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
INVALID_ADDRESS = ung\u00FCltige adresse
|
INVALID_ADDRESS = ungültige adresse
|
||||||
|
|
||||||
INVALID_ASSET_ID = ung\u00FCltige asset ID
|
INVALID_ASSET_ID = ungültige asset ID
|
||||||
|
|
||||||
INVALID_DATA = ung\u00FCltige daten
|
INVALID_DATA = ungültige daten
|
||||||
|
|
||||||
INVALID_PUBLIC_KEY = ung\u00FCltiger public key
|
INVALID_PUBLIC_KEY = ungültiger public key
|
||||||
|
|
||||||
INVALID_SIGNATURE = ung\u00FCltige signatur
|
INVALID_SIGNATURE = ungültige signatur
|
||||||
|
|
||||||
JSON = JSON nachricht konnte nicht geparsed werden
|
JSON = JSON nachricht konnte nicht geparsed werden
|
||||||
|
|
||||||
|
@ -1,53 +1,57 @@
|
|||||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||||
# Keys are from api.ApiError enum
|
# Keys are from api.ApiError enum
|
||||||
|
|
||||||
ADDRESS_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0083\u00D1\u0087\u00D0\u00B5\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
|
ADDRESS_UNKNOWN = неизвестная учетная запись
|
||||||
|
|
||||||
|
BLOCKCHAIN_NEEDS_SYNC = блокчейн должен сначала синхронизироваться
|
||||||
|
|
||||||
# Blocks
|
# Blocks
|
||||||
BLOCK_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA
|
BLOCK_UNKNOWN = неизвестный блок
|
||||||
|
|
||||||
CANNOT_MINT = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082\u00D1\u008C
|
CANNOT_MINT = аккаунт не может чеканить
|
||||||
|
|
||||||
GROUP_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0
|
GROUP_UNKNOWN = неизвестная группа
|
||||||
|
|
||||||
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
INVALID_ADDRESS = неизвестный адрес
|
||||||
|
|
||||||
# Assets
|
# Assets
|
||||||
INVALID_ASSET_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
INVALID_ASSET_ID = неверный идентификатор актива
|
||||||
|
|
||||||
INVALID_CRITERIA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00BA\u00D1\u0080\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D1\u0080\u00D0\u00B8\u00D0\u00B8 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00B0
|
INVALID_CRITERIA = неверные критерии поиска
|
||||||
|
|
||||||
INVALID_DATA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00BD\u00D1\u008B\u00D0\u00B5
|
INVALID_DATA = неверные данные
|
||||||
|
|
||||||
INVALID_HEIGHT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B2\u00D1\u008B\u00D1\u0081\u00D0\u00BE\u00D1\u0082\u00D0\u00B0 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D0\u00B0
|
INVALID_HEIGHT = недопустимая высота блока
|
||||||
|
|
||||||
INVALID_NETWORK_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00B5\u00D1\u0082\u00D0\u00B5\u00D0\u00B2\u00D0\u00BE\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
INVALID_NETWORK_ADDRESS = неверный сетевой адрес
|
||||||
|
|
||||||
INVALID_ORDER_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
INVALID_ORDER_ID = неверный идентификатор заказа актива
|
||||||
|
|
||||||
INVALID_PRIVATE_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
INVALID_PRIVATE_KEY = неверный приватный ключ
|
||||||
|
|
||||||
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
INVALID_PUBLIC_KEY = недействительный открытый ключ
|
||||||
|
|
||||||
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
|
INVALID_REFERENCE = неверная ссылка
|
||||||
|
|
||||||
# Validation
|
# Validation
|
||||||
INVALID_SIGNATURE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
|
INVALID_SIGNATURE = недействительная подпись
|
||||||
|
|
||||||
JSON = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D0\u00B1\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 json
|
JSON = не удалось разобрать сообщение json
|
||||||
|
|
||||||
NAME_UNKNOWN = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00BE
|
NAME_UNKNOWN = имя неизвестно
|
||||||
|
|
||||||
ORDER_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
NON_PRODUCTION = этот вызов API не разрешен для производственных систем
|
||||||
|
|
||||||
PUBLIC_KEY_NOT_FOUND = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B9\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD
|
ORDER_UNKNOWN = неизвестный идентификатор заказа актива
|
||||||
|
|
||||||
REPOSITORY_ISSUE = \u00D0\u00BE\u00D1\u0088\u00D0\u00B8\u00D0\u00B1\u00D0\u00BA\u00D0\u00B0 \u00D1\u0080\u00D0\u00B5\u00D0\u00BF\u00D0\u00BE\u00D0\u00B7\u00D0\u00B8\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D1\u008F
|
PUBLIC_KEY_NOT_FOUND = открытый ключ не найден
|
||||||
|
|
||||||
TRANSACTION_INVALID = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0: %s (%s)
|
REPOSITORY_ISSUE = ошибка репозитория
|
||||||
|
|
||||||
TRANSACTION_UNKNOWN = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0
|
TRANSACTION_INVALID = транзакция недействительна: %s (%s)
|
||||||
|
|
||||||
TRANSFORMATION_ERROR = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C JSON \u00D0\u00B2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008E
|
TRANSACTION_UNKNOWN = транзакция неизвестна
|
||||||
|
|
||||||
UNAUTHORIZED = \u00D0\u00B2\u00D1\u008B\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2 API \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B2\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD
|
TRANSFORMATION_ERROR = не удалось преобразовать JSON в транзакцию
|
||||||
|
|
||||||
|
UNAUTHORIZED = вызов API не авторизован
|
||||||
|
@ -19,6 +19,8 @@ CREATING_BACKUP_OF_DB_FILES = Creating backup of database files...
|
|||||||
|
|
||||||
DB_BACKUP = Database Backup
|
DB_BACKUP = Database Backup
|
||||||
|
|
||||||
|
DB_CHECKPOINT = Database Checkpoint
|
||||||
|
|
||||||
EXIT = Exit
|
EXIT = Exit
|
||||||
|
|
||||||
MINTING_DISABLED = NOT minting
|
MINTING_DISABLED = NOT minting
|
||||||
@ -34,6 +36,8 @@ NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
|
|||||||
|
|
||||||
OPEN_UI = Open UI
|
OPEN_UI = Open UI
|
||||||
|
|
||||||
|
PERFORMING_DB_CHECKPOINT = Saving uncommitted database changes...
|
||||||
|
|
||||||
SYNCHRONIZE_CLOCK = Synchronize clock
|
SYNCHRONIZE_CLOCK = Synchronize clock
|
||||||
|
|
||||||
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
|
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||||
# SysTray pop-up menu
|
# SysTray pop-up menu
|
||||||
|
|
||||||
BLOCK_HEIGHT = \u5757\u9AD8\u5EA6
|
BLOCK_HEIGHT = 块高度
|
||||||
|
|
||||||
CHECK_TIME_ACCURACY = \u68C0\u67E5\u65F6\u95F4\u51C6\u786E\u6027
|
CHECK_TIME_ACCURACY = 检查时间准确性
|
||||||
|
|
||||||
CONNECTION = \u4E2A\u8FDE\u63A5
|
CONNECTION = 个连接
|
||||||
|
|
||||||
CONNECTIONS = \u4E2A\u8FDE\u63A5
|
CONNECTIONS = 个连接
|
||||||
|
|
||||||
EXIT = \u9000\u51FA\u8F6F\u4EF6
|
EXIT = 退出软件
|
||||||
|
|
||||||
MINTING_DISABLED = \u6CA1\u6709\u94F8\u5E01
|
MINTING_DISABLED = 没有铸币
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 \u94F8\u5E01
|
MINTING_ENABLED = ✔ 铸币
|
||||||
|
|
||||||
# Nagging about lack of NTP time sync
|
# Nagging about lack of NTP time sync
|
||||||
NTP_NAG_CAPTION = \u7535\u8111\u7684\u65F6\u949F\u4E0D\u51C6\u786E\uFF01
|
NTP_NAG_CAPTION = 电脑的时钟不准确!
|
||||||
|
|
||||||
NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7684\u65F6\u949F\u3002
|
NTP_NAG_TEXT_UNIX = 安装NTP服务以获得准确的时钟。
|
||||||
|
|
||||||
NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002
|
NTP_NAG_TEXT_WINDOWS = 从菜单中选择“同步时钟”进行修复。
|
||||||
|
|
||||||
OPEN_UI = \u5F00\u542F\u754C\u9762
|
OPEN_UI = 开启界面
|
||||||
|
|
||||||
SYNCHRONIZE_CLOCK = \u540C\u6B65\u65F6\u949F
|
SYNCHRONIZE_CLOCK = 同步时钟
|
||||||
|
|
||||||
SYNCHRONIZING_BLOCKCHAIN = \u540C\u6B65\u533A\u5757\u94FE
|
SYNCHRONIZING_BLOCKCHAIN = 同步区块链
|
||||||
|
|
||||||
SYNCHRONIZING_CLOCK = \u540C\u6B65\u7740\u65F6\u949F
|
SYNCHRONIZING_CLOCK = 同步着时钟
|
||||||
|
@ -1,164 +1,176 @@
|
|||||||
|
|
||||||
ACCOUNT_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
ACCOUNT_ALREADY_EXISTS = аккаунт уже существует
|
||||||
|
|
||||||
ACCOUNT_CANNOT_REWARD_SHARE = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5\u00D0\u00BC
|
ACCOUNT_CANNOT_REWARD_SHARE = аккаунт не может делиться вознаграждением
|
||||||
|
|
||||||
ALREADY_GROUP_ADMIN = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
ALREADY_GROUP_ADMIN = уже администратор группы
|
||||||
|
|
||||||
ALREADY_GROUP_MEMBER = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
ALREADY_GROUP_MEMBER = уже член группы
|
||||||
|
|
||||||
ALREADY_VOTED_FOR_THAT_OPTION = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B3\u00D0\u00BE\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BB\u00D0\u00B8 \u00D0\u00B7\u00D0\u00B0 \u00D1\u008D\u00D1\u0082\u00D0\u00BE\u00D1\u0082 \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
ALREADY_VOTED_FOR_THAT_OPTION = уже проголосовали за этот вариант
|
||||||
|
|
||||||
ASSET_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
ASSET_ALREADY_EXISTS = актив уже существует
|
||||||
|
|
||||||
ASSET_DOES_NOT_EXIST = \u00D0\u0090\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
ASSET_DOES_NOT_EXIST = Актив не существует
|
||||||
|
|
||||||
ASSET_DOES_NOT_MATCH_AT = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00BF\u00D0\u00B0\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082 \u00D1\u0081 \u00D0\u0090\u00D0\u00A2
|
ASSET_DOES_NOT_MATCH_AT = актив не совпадает с АТ
|
||||||
|
|
||||||
AT_ALREADY_EXISTS = AT \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
ASSET_NOT_SPENDABLE = актив не подлежит расходованию
|
||||||
|
|
||||||
AT_IS_FINISHED = AT \u00D0\u00B2 \u00D0\u00B7\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B8
|
AT_ALREADY_EXISTS = AT уже существует
|
||||||
|
|
||||||
AT_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u0090\u00D0\u00A2
|
AT_IS_FINISHED = AT в завершении
|
||||||
|
|
||||||
BANNED_FROM_GROUP = \u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087\u00D0\u00B5\u00D0\u00BD \u00D0\u00B8\u00D0\u00B7 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
AT_UNKNOWN = не известный АТ
|
||||||
|
|
||||||
BAN_EXISTS = \u00D0\u0091\u00D0\u00B0\u00D0\u00BD
|
BANNED_FROM_GROUP = исключен из группы
|
||||||
|
|
||||||
BAN_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BD
|
BAN_EXISTS = Бан
|
||||||
|
|
||||||
BUYER_ALREADY_OWNER = \u00D0\u00BF\u00D0\u00BE\u00D0\u00BA\u00D1\u0083\u00D0\u00BF\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B1\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D0\u00BD\u00D0\u00BD\u00D0\u00B8\u00D0\u00BA
|
BAN_UNKNOWN = не известный бан
|
||||||
|
|
||||||
DUPLICATE_OPTION = \u00D0\u00B4\u00D1\u0083\u00D0\u00B1\u00D0\u00BB\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
BUYER_ALREADY_OWNER = покупатель уже собственник
|
||||||
|
|
||||||
GROUP_ALREADY_EXISTS = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
CLOCK_NOT_SYNCED = часы не синхронизированы
|
||||||
|
|
||||||
GROUP_APPROVAL_DECIDED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
DUPLICATE_OPTION = дублировать вариант
|
||||||
|
|
||||||
GROUP_APPROVAL_NOT_REQUIRED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00BE\u00D0\u00B2\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0082\u00D1\u0080\u00D0\u00B5\u00D0\u00B1\u00D1\u0083\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
GROUP_ALREADY_EXISTS = группа уже существует
|
||||||
|
|
||||||
GROUP_DOES_NOT_EXIST = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
GROUP_APPROVAL_DECIDED = гуппа одобрена
|
||||||
|
|
||||||
GROUP_ID_MISMATCH = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
GROUP_APPROVAL_NOT_REQUIRED = гупповое одобрение не требуется
|
||||||
|
|
||||||
GROUP_OWNER_CANNOT_LEAVE = \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0083\u00D0\u00B9\u00D1\u0082\u00D0\u00B8
|
GROUP_DOES_NOT_EXIST = группа не существует
|
||||||
|
|
||||||
HAVE_EQUALS_WANT = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D0\u00B5\u00D1\u008E\u00D1\u0082\u00D1\u0081\u00D1\u008F \u00D1\u0080\u00D0\u00B0\u00D0\u00B2\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B6\u00D0\u00B5\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
GROUP_ID_MISMATCH = не соответствие идентификатора группы
|
||||||
|
|
||||||
INSUFFICIENT_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B0
|
GROUP_OWNER_CANNOT_LEAVE = владелец группы не может уйти
|
||||||
|
|
||||||
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
HAVE_EQUALS_WANT = иммеются равные желания
|
||||||
|
|
||||||
INVALID_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
INSUFFICIENT_FEE = недостаточная плата
|
||||||
|
|
||||||
INVALID_ASSET_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
INVALID_ADDRESS = недействительный адрес
|
||||||
|
|
||||||
INVALID_AT_TRANSACTION = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u0090\u00D0\u00A2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
INVALID_AMOUNT = недопустимая сумма
|
||||||
|
|
||||||
INVALID_AT_TYPE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE \u00D0\u00B4\u00D0\u00BB\u00D1\u008F \u00D1\u0082\u00D0\u00B8\u00D0\u00BF\u00D0\u00B0 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B AT
|
INVALID_ASSET_OWNER = недействительный владелец актива
|
||||||
|
|
||||||
INVALID_CREATION_BYTES = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B5 \u00D0\u00B1\u00D0\u00B0\u00D0\u00B9\u00D1\u0082\u00D1\u008B \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
INVALID_AT_TRANSACTION = недействительная АТ транзакция
|
||||||
|
|
||||||
INVALID_DESCRIPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
INVALID_AT_TYPE_LENGTH = недействительно для типа длины AT
|
||||||
|
|
||||||
INVALID_GROUP_APPROVAL_THRESHOLD = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D1\u0080\u00D0\u00BE\u00D0\u00B3 \u00D1\u0083\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
INVALID_CREATION_BYTES = недопустимые байты создания
|
||||||
|
|
||||||
INVALID_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
INVALID_DATA_LENGTH = недопустимая длина данных
|
||||||
|
|
||||||
INVALID_GROUP_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
INVALID_DESCRIPTION_LENGTH = недопустимая длина описания
|
||||||
|
|
||||||
INVALID_LIFETIME = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D1\u0081\u00D1\u0080\u00D0\u00BE\u00D0\u00BA \u00D1\u0081\u00D0\u00BB\u00D1\u0083\u00D0\u00B6\u00D0\u00B1\u00D1\u008B
|
INVALID_GROUP_APPROVAL_THRESHOLD = недопустимый порог утверждения группы
|
||||||
|
|
||||||
INVALID_NAME_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
INVALID_GROUP_ID = недопустимый идентификатор группы
|
||||||
|
|
||||||
INVALID_NAME_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D1\u0086\u00D0\u00B0
|
INVALID_GROUP_OWNER = недопу владелец группы
|
||||||
|
|
||||||
INVALID_OPTIONS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B9
|
INVALID_LIFETIME = недопу срок службы
|
||||||
|
|
||||||
INVALID_OPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B8
|
INVALID_NAME_LENGTH = недопустимая длина группы
|
||||||
|
|
||||||
INVALID_ORDER_CREATOR = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0
|
INVALID_NAME_OWNER = недопустимое имя владельца
|
||||||
|
|
||||||
INVALID_PAYMENTS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00B6\u00D0\u00B5\u00D0\u00B9
|
INVALID_OPTIONS_COUNT = неверное количество опций
|
||||||
|
|
||||||
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
INVALID_OPTION_LENGTH = недопустимая длина опции
|
||||||
|
|
||||||
INVALID_QUANTITY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE
|
INVALID_ORDER_CREATOR = недопустимый создатель заказа
|
||||||
|
|
||||||
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
|
INVALID_PAYMENTS_COUNT = неверный подсчет платежей
|
||||||
|
|
||||||
INVALID_RETURN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00B2\u00D1\u0080\u00D0\u00B0\u00D1\u0082
|
INVALID_PUBLIC_KEY = недействительный открытый ключ
|
||||||
|
|
||||||
INVALID_REWARD_SHARE_PERCENT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0086\u00D0\u00B5\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
INVALID_QUANTITY = недопустимое количество
|
||||||
|
|
||||||
INVALID_SELLER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0086
|
INVALID_REFERENCE = неверная ссылка
|
||||||
|
|
||||||
INVALID_TAGS_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D1\u0082\u00D1\u008D\u00D0\u00B3\u00D0\u00BE\u00D0\u00B2
|
INVALID_RETURN = недопустимый возврат
|
||||||
|
|
||||||
INVALID_TX_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BF\u00D0\u00B5\u00D1\u0080\u00D0\u00B5\u00D0\u00B4\u00D0\u00B0\u00D1\u0087\u00D0\u00B8
|
INVALID_REWARD_SHARE_PERCENT = недействительный процент награждения
|
||||||
|
|
||||||
INVALID_VALUE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D1\u0087\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B
|
INVALID_SELLER = недействительный продавец
|
||||||
|
|
||||||
JOIN_REQUEST_EXISTS = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D0\u00BD\u00D0\u00B0 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D1\u0081\u00D0\u00BE\u00D0\u00B5\u00D0\u00B4\u00D0\u00B8\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
INVALID_TAGS_LENGTH = недействительная длина тэгов
|
||||||
|
|
||||||
MAXIMUM_REWARD_SHARES = \u00D0\u00BC\u00D0\u00B0\u00D0\u00BA\u00D1\u0081\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
|
INVALID_TX_GROUP_ID = недействительный идентификатор группы передачи
|
||||||
|
|
||||||
MISSING_CREATOR = \u00D0\u00BE\u00D1\u0082\u00D1\u0081\u00D1\u0083\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D1\u008E\u00D1\u0089\u00D0\u00B8\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C
|
INVALID_VALUE_LENGTH = недопустимое значение длины
|
||||||
|
|
||||||
MULTIPLE_NAMES_FORBIDDEN = \u00D0\u00BD\u00D0\u00B5\u00D1\u0081\u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D1\u008C\u00D0\u00BA\u00D0\u00BE \u00D0\u00B8\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
INVITE_UNKNOWN = приглашать неизветсных
|
||||||
|
|
||||||
NAME_ALREADY_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B2 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B6\u00D0\u00B5
|
JOIN_REQUEST_EXISTS = запрос на присоединение существует
|
||||||
|
|
||||||
NAME_ALREADY_REGISTERED = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B7\u00D0\u00B0\u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD\u00D0\u00BE
|
MAXIMUM_REWARD_SHARES = максимальное вознаграждение
|
||||||
|
|
||||||
NAME_DOES_NOT_EXIST = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
MISSING_CREATOR = отсутствующий создатель
|
||||||
|
|
||||||
NAME_NOT_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
MULTIPLE_NAMES_FORBIDDEN = несколько имен запрещено
|
||||||
|
|
||||||
NAME_NOT_LOWER_CASE = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B6\u00D0\u00BD\u00D0\u00BE \u00D1\u0081\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080
|
NAME_ALREADY_FOR_SALE = имя уже в продаже
|
||||||
|
|
||||||
NEGATIVE_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
NAME_ALREADY_REGISTERED = имя уже зарегистрировано
|
||||||
|
|
||||||
NEGATIVE_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BA\u00D0\u00BE\u00D0\u00BC\u00D0\u00B8\u00D1\u0081\u00D1\u0081\u00D0\u00B8\u00D1\u008F
|
NAME_DOES_NOT_EXIST = имя не существует
|
||||||
|
|
||||||
NEGATIVE_PRICE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D1\u008C
|
NAME_NOT_FOR_SALE = имя не продается
|
||||||
|
|
||||||
NOT_GROUP_ADMIN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
NAME_NOT_LOWER_CASE = иммя не должно содержать строчный регистр
|
||||||
|
|
||||||
NOT_GROUP_MEMBER = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
NEGATIVE_AMOUNT = недостаточная сумма
|
||||||
|
|
||||||
NOT_MINTING_ACCOUNT = \u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082
|
NEGATIVE_FEE = недостаточная комиссия
|
||||||
|
|
||||||
NOT_YET_RELEASED = \u00D0\u00B5\u00D1\u0089\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B2\u00D1\u008B\u00D0\u00BF\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
NEGATIVE_PRICE = недостаточная стоимость
|
||||||
|
|
||||||
NO_BALANCE = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D1\u0081\u00D0\u00B0
|
NOT_GROUP_ADMIN = не администратор группы
|
||||||
|
|
||||||
NO_BLOCKCHAIN_LOCK = \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D1\u0087\u00D0\u00B5\u00D0\u00B9\u00D0\u00BD \u00D1\u0083\u00D0\u00B7\u00D0\u00BB\u00D0\u00B0 \u00D0\u00B2 \u00D0\u00BD\u00D0\u00B0\u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D1\u008F\u00D1\u0089\u00D0\u00B5\u00D0\u00B5 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BD\u00D1\u008F\u00D1\u0082
|
NOT_GROUP_MEMBER = не член группы
|
||||||
|
|
||||||
NO_FLAG_PERMISSION = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D1\u0080\u00D0\u00B5\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B0 \u00D1\u0084\u00D0\u00BB\u00D0\u00B0\u00D0\u00B3
|
NOT_MINTING_ACCOUNT = счет не чеканит
|
||||||
|
|
||||||
|
NOT_YET_RELEASED = еще не выпущено
|
||||||
|
|
||||||
|
NO_BALANCE = нет баланса
|
||||||
|
|
||||||
|
NO_BLOCKCHAIN_LOCK = блокчейн узла в настоящее время занят
|
||||||
|
|
||||||
|
NO_FLAG_PERMISSION = нет разрешения на флаг
|
||||||
|
|
||||||
OK = OK
|
OK = OK
|
||||||
|
|
||||||
ORDER_ALREADY_CLOSED = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082
|
ORDER_ALREADY_CLOSED = заказ закрыт
|
||||||
|
|
||||||
ORDER_DOES_NOT_EXIST = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
ORDER_DOES_NOT_EXIST = заказа не существует
|
||||||
|
|
||||||
POLL_ALREADY_EXISTS = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
POLL_ALREADY_EXISTS = опрос уже существует
|
||||||
|
|
||||||
POLL_DOES_NOT_EXIST = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
POLL_DOES_NOT_EXIST = опроса не существует
|
||||||
|
|
||||||
POLL_OPTION_DOES_NOT_EXIST = \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082\u00D0\u00BE\u00D0\u00B2 \u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
POLL_OPTION_DOES_NOT_EXIST = вариантов ответа не существует
|
||||||
|
|
||||||
PUBLIC_KEY_UNKNOWN = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B5\u00D0\u00BD
|
PUBLIC_KEY_UNKNOWN = открытый ключ неизвестен
|
||||||
|
|
||||||
SELF_SHARE_EXISTS = \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B5\u00D0\u00B9
|
SELF_SHARE_EXISTS = поделиться долей
|
||||||
|
|
||||||
TIMESTAMP_TOO_NEW = \u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
TIMESTAMP_TOO_NEW = новая метка времени
|
||||||
|
|
||||||
TIMESTAMP_TOO_OLD = \u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0080\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
TIMESTAMP_TOO_OLD = старая метка времени
|
||||||
|
|
||||||
TRANSACTION_ALREADY_CONFIRMED = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
TOO_MANY_UNCONFIRMED = много не подтвержденных
|
||||||
|
|
||||||
TRANSACTION_ALREADY_EXISTS = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
TRANSACTION_ALREADY_CONFIRMED = транзакция уже подтверждена
|
||||||
|
|
||||||
TRANSACTION_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
TRANSACTION_ALREADY_EXISTS = транзакция существует
|
||||||
|
|
||||||
|
TRANSACTION_UNKNOWN = неизвестная транзакция
|
||||||
|
|
||||||
|
TX_GROUP_ID_MISMATCH = не соответствие идентификатора группы c хэш транзации
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package org.qortal.test;
|
package org.qortal.test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -133,6 +137,129 @@ public class BlockTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLatestBlockCacheWithLatestBlock() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||||
|
|
||||||
|
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||||
|
byte[] parentSignature = latestBlock.getSignature();
|
||||||
|
|
||||||
|
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||||
|
|
||||||
|
assertEquals(true, childBlocks.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLatestBlockCacheWithPenultimateBlock() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||||
|
|
||||||
|
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||||
|
BlockData penultimateBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - 1);
|
||||||
|
byte[] parentSignature = penultimateBlock.getSignature();
|
||||||
|
|
||||||
|
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||||
|
|
||||||
|
assertEquals(false, childBlocks.isEmpty());
|
||||||
|
assertEquals(1, childBlocks.size());
|
||||||
|
|
||||||
|
BlockData expectedBlock = latestBlock;
|
||||||
|
BlockData actualBlock = childBlocks.get(0);
|
||||||
|
assertArrayEquals(expectedBlock.getSignature(), actualBlock.getSignature());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLatestBlockCacheWithMiddleBlock() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||||
|
|
||||||
|
int tipOffset = 5;
|
||||||
|
|
||||||
|
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||||
|
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
|
||||||
|
byte[] parentSignature = parentBlock.getSignature();
|
||||||
|
|
||||||
|
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||||
|
|
||||||
|
assertEquals(false, childBlocks.isEmpty());
|
||||||
|
assertEquals(tipOffset, childBlocks.size());
|
||||||
|
|
||||||
|
BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1);
|
||||||
|
BlockData actualFirstBlock = childBlocks.get(0);
|
||||||
|
assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature());
|
||||||
|
|
||||||
|
BlockData expectedLastBlock = latestBlock;
|
||||||
|
BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1);
|
||||||
|
assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLatestBlockCacheWithFirstBlock() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||||
|
|
||||||
|
int tipOffset = latestBlockCache.size();
|
||||||
|
|
||||||
|
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||||
|
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
|
||||||
|
byte[] parentSignature = parentBlock.getSignature();
|
||||||
|
|
||||||
|
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||||
|
|
||||||
|
assertEquals(false, childBlocks.isEmpty());
|
||||||
|
assertEquals(tipOffset, childBlocks.size());
|
||||||
|
|
||||||
|
BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1);
|
||||||
|
BlockData actualFirstBlock = childBlocks.get(0);
|
||||||
|
assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature());
|
||||||
|
|
||||||
|
BlockData expectedLastBlock = latestBlock;
|
||||||
|
BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1);
|
||||||
|
assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLatestBlockCacheWithNoncachedBlock() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||||
|
|
||||||
|
int tipOffset = latestBlockCache.size() + 1; // outside of cache
|
||||||
|
|
||||||
|
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||||
|
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
|
||||||
|
byte[] parentSignature = parentBlock.getSignature();
|
||||||
|
|
||||||
|
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||||
|
|
||||||
|
assertEquals(true, childBlocks.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Deque<BlockData> buildLatestBlockCache(Repository repository, int count) throws DataException {
|
||||||
|
Deque<BlockData> latestBlockCache = new LinkedList<>();
|
||||||
|
|
||||||
|
// Mint some blocks
|
||||||
|
for (int h = 0; h < count; ++h)
|
||||||
|
latestBlockCache.addLast(BlockUtils.mintBlock(repository).getBlockData());
|
||||||
|
|
||||||
|
// Reduce cache down to latest 10 blocks
|
||||||
|
while (latestBlockCache.size() > 10)
|
||||||
|
latestBlockCache.removeFirst();
|
||||||
|
|
||||||
|
return latestBlockCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BlockData> findCachedChildBlocks(Deque<BlockData> latestBlockCache, byte[] parentSignature) {
|
||||||
|
return latestBlockCache.stream()
|
||||||
|
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCommonBlockSearch() {
|
public void testCommonBlockSearch() {
|
||||||
// Given a list of block summaries, trim all trailing summaries after common block
|
// Given a list of block summaries, trim all trailing summaries after common block
|
||||||
|
@ -103,8 +103,10 @@ public class ChainWeightTests extends Common {
|
|||||||
populateBlockSummariesMinterLevels(repository, shorterChain);
|
populateBlockSummariesMinterLevels(repository, shorterChain);
|
||||||
populateBlockSummariesMinterLevels(repository, longerChain);
|
populateBlockSummariesMinterLevels(repository, longerChain);
|
||||||
|
|
||||||
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain);
|
final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size());
|
||||||
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain);
|
|
||||||
|
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain, mutualHeight);
|
||||||
|
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain, mutualHeight);
|
||||||
|
|
||||||
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
|
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ public class RepositoryTests extends Common {
|
|||||||
BlockUtils.mintBlock(repository1);
|
BlockUtils.mintBlock(repository1);
|
||||||
|
|
||||||
// Perform database 'update', but don't commit at this stage
|
// Perform database 'update', but don't commit at this stage
|
||||||
repository1.getBlockRepository().trimOldOnlineAccountsSignatures(System.currentTimeMillis());
|
repository1.getBlockRepository().trimOldOnlineAccountsSignatures(1, 10);
|
||||||
|
|
||||||
// Open connection 2
|
// Open connection 2
|
||||||
try (final Repository repository2 = RepositoryManager.getRepository()) {
|
try (final Repository repository2 = RepositoryManager.getRepository()) {
|
||||||
|
426
src/test/java/org/qortal/test/at/AtRepositoryTests.java
Normal file
426
src/test/java/org/qortal/test/at/AtRepositoryTests.java
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
package org.qortal.test.at;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.ciyam.at.CompilationException;
|
||||||
|
import org.ciyam.at.MachineState;
|
||||||
|
import org.ciyam.at.OpCode;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.data.at.ATData;
|
||||||
|
import org.qortal.data.at.ATStateData;
|
||||||
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
|
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.group.Group;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.test.common.BlockUtils;
|
||||||
|
import org.qortal.test.common.Common;
|
||||||
|
import org.qortal.test.common.TransactionUtils;
|
||||||
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
|
|
||||||
|
public class AtRepositoryTests extends Common {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() throws DataException {
|
||||||
|
Common.useDefaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetATStateAtHeightWithData() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
Integer testHeight = 8;
|
||||||
|
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetATStateAtHeightWithoutData() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
int maxHeight = 8;
|
||||||
|
Integer testHeight = maxHeight - 2;
|
||||||
|
|
||||||
|
// Trim AT state data
|
||||||
|
repository.getATRepository().prepareForAtStateTrimming();
|
||||||
|
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||||
|
|
||||||
|
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetLatestATStateWithData() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||||
|
|
||||||
|
Integer testHeight = blockchainHeight;
|
||||||
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetLatestATStatePostTrimming() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||||
|
|
||||||
|
int maxHeight = blockchainHeight + 100; // more than latest block height
|
||||||
|
Integer testHeight = blockchainHeight;
|
||||||
|
|
||||||
|
// Trim AT state data
|
||||||
|
repository.getATRepository().prepareForAtStateTrimming();
|
||||||
|
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||||
|
|
||||||
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
// We should always have the latest AT state data available
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||||
|
|
||||||
|
Integer testHeight = blockchainHeight;
|
||||||
|
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
|
||||||
|
byte[] codeHash = atData.getCodeHash();
|
||||||
|
Boolean isFinished = Boolean.FALSE;
|
||||||
|
Integer dataByteOffset = null;
|
||||||
|
Long expectedValue = null;
|
||||||
|
Integer minimumFinalHeight = null;
|
||||||
|
Integer limit = null;
|
||||||
|
Integer offset = null;
|
||||||
|
Boolean reverse = null;
|
||||||
|
|
||||||
|
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||||
|
codeHash,
|
||||||
|
isFinished,
|
||||||
|
dataByteOffset,
|
||||||
|
expectedValue,
|
||||||
|
minimumFinalHeight,
|
||||||
|
limit, offset, reverse);
|
||||||
|
|
||||||
|
assertEquals(false, atStates.isEmpty());
|
||||||
|
assertEquals(1, atStates.size());
|
||||||
|
|
||||||
|
ATStateData atStateData = atStates.get(0);
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetMatchingFinalATStatesWithDataValue() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||||
|
|
||||||
|
Integer testHeight = blockchainHeight;
|
||||||
|
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
|
||||||
|
byte[] codeHash = atData.getCodeHash();
|
||||||
|
Boolean isFinished = Boolean.FALSE;
|
||||||
|
Integer dataByteOffset = MachineState.HEADER_LENGTH + 0;
|
||||||
|
Long expectedValue = 0L;
|
||||||
|
Integer minimumFinalHeight = null;
|
||||||
|
Integer limit = null;
|
||||||
|
Integer offset = null;
|
||||||
|
Boolean reverse = null;
|
||||||
|
|
||||||
|
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||||
|
codeHash,
|
||||||
|
isFinished,
|
||||||
|
dataByteOffset,
|
||||||
|
expectedValue,
|
||||||
|
minimumFinalHeight,
|
||||||
|
limit, offset, reverse);
|
||||||
|
|
||||||
|
assertEquals(false, atStates.isEmpty());
|
||||||
|
assertEquals(1, atStates.size());
|
||||||
|
|
||||||
|
ATStateData atStateData = atStates.get(0);
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetBlockATStatesAtHeightWithData() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
Integer testHeight = 8;
|
||||||
|
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
|
||||||
|
|
||||||
|
assertEquals(false, atStates.isEmpty());
|
||||||
|
assertEquals(1, atStates.size());
|
||||||
|
|
||||||
|
ATStateData atStateData = atStates.get(0);
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
// getBlockATStatesAtHeight never returns actual AT state data anyway
|
||||||
|
assertNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetBlockATStatesAtHeightWithoutData() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
int maxHeight = 8;
|
||||||
|
Integer testHeight = maxHeight - 2;
|
||||||
|
|
||||||
|
// Trim AT state data
|
||||||
|
repository.getATRepository().prepareForAtStateTrimming();
|
||||||
|
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||||
|
|
||||||
|
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
|
||||||
|
|
||||||
|
assertEquals(false, atStates.isEmpty());
|
||||||
|
assertEquals(1, atStates.size());
|
||||||
|
|
||||||
|
ATStateData atStateData = atStates.get(0);
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
// getBlockATStatesAtHeight never returns actual AT state data anyway
|
||||||
|
assertNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveATStateWithData() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||||
|
|
||||||
|
Integer testHeight = blockchainHeight - 2;
|
||||||
|
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
|
||||||
|
repository.getATRepository().save(atStateData);
|
||||||
|
|
||||||
|
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveATStateWithoutData() throws DataException {
|
||||||
|
byte[] creationBytes = buildSimpleAT();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||||
|
|
||||||
|
long fundingAmount = 1_00000000L;
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||||
|
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||||
|
|
||||||
|
// Mint a few blocks
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||||
|
|
||||||
|
Integer testHeight = blockchainHeight - 2;
|
||||||
|
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNotNull(atStateData.getStateData());
|
||||||
|
|
||||||
|
// Clear data
|
||||||
|
ATStateData newAtStateData = new ATStateData(atStateData.getATAddress(),
|
||||||
|
atStateData.getHeight(),
|
||||||
|
/*StateData*/ null,
|
||||||
|
atStateData.getStateHash(),
|
||||||
|
atStateData.getFees(),
|
||||||
|
atStateData.isInitial());
|
||||||
|
repository.getATRepository().save(newAtStateData);
|
||||||
|
|
||||||
|
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||||
|
|
||||||
|
assertEquals(testHeight, atStateData.getHeight());
|
||||||
|
assertNull(atStateData.getStateData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildSimpleAT() {
|
||||||
|
// Pretend we use 4 values in data segment
|
||||||
|
int addrCounter = 4;
|
||||||
|
|
||||||
|
// Data segment
|
||||||
|
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||||
|
|
||||||
|
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||||
|
|
||||||
|
// Two-pass version
|
||||||
|
for (int pass = 0; pass < 2; ++pass) {
|
||||||
|
codeByteBuffer.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop and wait for next block
|
||||||
|
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||||
|
} catch (CompilationException e) {
|
||||||
|
throw new IllegalStateException("Unable to compile AT?", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeByteBuffer.flip();
|
||||||
|
|
||||||
|
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||||
|
codeByteBuffer.get(codeBytes);
|
||||||
|
|
||||||
|
final short ciyamAtVersion = 2;
|
||||||
|
final short numCallStackPages = 0;
|
||||||
|
final short numUserStackPages = 0;
|
||||||
|
final long minActivationAmount = 0L;
|
||||||
|
|
||||||
|
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||||
|
long txTimestamp = System.currentTimeMillis();
|
||||||
|
byte[] lastReference = deployer.getLastReference();
|
||||||
|
|
||||||
|
if (lastReference == null) {
|
||||||
|
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long fee = null;
|
||||||
|
String name = "Test AT";
|
||||||
|
String description = "Test AT";
|
||||||
|
String atType = "Test";
|
||||||
|
String tags = "TEST";
|
||||||
|
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||||
|
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||||
|
|
||||||
|
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||||
|
|
||||||
|
fee = deployAtTransaction.calcRecommendedFee();
|
||||||
|
deployAtTransactionData.setFee(fee);
|
||||||
|
|
||||||
|
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||||
|
|
||||||
|
return deployAtTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -13,7 +13,7 @@ fi
|
|||||||
cd ${git_dir}
|
cd ${git_dir}
|
||||||
|
|
||||||
# Check we are in 'master' branch
|
# Check we are in 'master' branch
|
||||||
branch_name=$( git symbolic-ref -q HEAD )
|
branch_name=$( git symbolic-ref -q HEAD || echo )
|
||||||
branch_name=${branch_name##refs/heads/}
|
branch_name=${branch_name##refs/heads/}
|
||||||
echo "Current git branch: ${branch_name}"
|
echo "Current git branch: ${branch_name}"
|
||||||
if [ "${branch_name}" != "master" ]; then
|
if [ "${branch_name}" != "master" ]; then
|
||||||
@ -78,5 +78,7 @@ git add ${project}.update
|
|||||||
git commit --message "XORed, auto-update JAR based on commit ${short_hash}"
|
git commit --message "XORed, auto-update JAR based on commit ${short_hash}"
|
||||||
git push --set-upstream origin --force-with-lease ${update_branch}
|
git push --set-upstream origin --force-with-lease ${update_branch}
|
||||||
|
|
||||||
|
branch_name=${branch_name-master}
|
||||||
|
|
||||||
echo "Changing back to '${branch_name}' branch"
|
echo "Changing back to '${branch_name}' branch"
|
||||||
git checkout --force ${branch_name}
|
git checkout --force ${branch_name}
|
||||||
|
Loading…
Reference in New Issue
Block a user