Merge branch 'master' into LTCv3-merge-test

This commit is contained in:
catbref 2020-11-13 10:46:45 +00:00
commit fccb3a3f0c
54 changed files with 2463 additions and 584 deletions

View File

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

Binary file not shown.

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -531,7 +531,8 @@ public class BlockChain {
private static void rebuildBlockchain() throws DataException { private static void rebuildBlockchain() throws DataException {
// (Re)build repository // (Re)build repository
RepositoryManager.rebuild(); if (!RepositoryManager.wasPristineAtOpen())
RepositoryManager.rebuild();
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository); GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
@ -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;

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

View File

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

View File

@ -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) {
if (blockData == null) BlockData blockData = this.latestBlocks.peekLast();
return 0; if (blockData == null)
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();
try (final Repository repository = RepositoryManager.getRepository()) { // If peer's parent signature matches our latest block signature
List<BlockSummaryData> blockSummaries = new ArrayList<>(); // then we can short-circuit with an empty response
BlockData chainTip = getChainTip();
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested()); if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
do {
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
if (blockData == null)
// No more blocks to send to peer
break;
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
blockSummaries.add(blockSummary);
parentSignature = blockData.getSignature();
} while (blockSummaries.size() < numberRequested);
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
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); return;
} }
List<BlockSummaryData> blockSummaries = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
blockSummaries = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockSummaryData::new)
.collect(Collectors.toList());
}
if (blockSummaries.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
while (blockData != null && blockSummaries.size() < numberRequested) {
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
blockSummaries.add(blockSummary);
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSummariesStats.cacheHits.incrementAndGet();
if (blockSummaries.size() >= getBlockSummariesMessage.getNumberRequested())
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
}
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
} }
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) { 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();
try (final Repository repository = RepositoryManager.getRepository()) { // If peer's parent signature matches our latest block signature
List<byte[]> signatures = new ArrayList<>(); // then we can short-circuit with an empty response
BlockData chainTip = getChainTip();
do { if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature); Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
if (blockData == null)
// No more signatures to send to peer
break;
parentSignature = blockData.getSignature();
signatures.add(parentSignature);
} while (signatures.size() < getSignaturesMessage.getNumberRequested());
Message signaturesMessage = new SignaturesMessage(signatures);
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); return;
} }
List<byte[]> signatures = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
signatures = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockData::getSignature)
.collect(Collectors.toList());
}
if (signatures.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = getSignaturesMessage.getNumberRequested();
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
while (blockData != null && signatures.size() < numberRequested) {
signatures.add(blockData.getSignature());
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSignaturesV2Stats.cacheHits.incrementAndGet();
if (signatures.size() >= getSignaturesMessage.getNumberRequested())
this.stats.getBlockSignaturesV2Stats.fullyFromCache.incrementAndGet();
}
Message signaturesMessage = new SignaturesMessage(signatures);
signaturesMessage.setId(message.getId());
if (!peer.sendMessage(signaturesMessage))
peer.disconnect("failed to send signatures (v2)");
} }
private void onNetworkHeightV2Message(Peer peer, Message message) { 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;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
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 { try {
saveHelper.execute(this.repository); 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);
} }

View File

@ -462,13 +462,46 @@ public class HSQLDBBlockRepository implements BlockRepository {
} }
@Override @Override
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException { public int getOnlineAccountsSignaturesTrimHeight() throws DataException {
String sql = "SELECT online_signatures_trim_height FROM DatabaseInfo";
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 {
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. // We're often called so no need to trim all blocks in one go.
// Limit updates to reduce CPU and memory load. // 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"; String sql = "UPDATE Blocks SET online_accounts_signatures = NULL "
+ "WHERE online_accounts_signatures IS NOT NULL "
+ "AND height BETWEEN ? AND ?";
try { try {
return this.repository.executeCheckedUpdate(sql, timestamp); 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);

View File

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

View File

@ -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,11 +312,12 @@ 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
@ -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,36 +639,35 @@ 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();
}
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
int[] updateCounts = preparedStatement.executeBatch();
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
logStatements();
}
}
int totalCount = 0;
for (int i = 0; i < updateCounts.length; ++i) {
if (updateCounts[i] < 0)
throw new SQLException("Database returned invalid row count");
totalCount += updateCounts[i];
}
return totalCount;
} }
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
int[] updateCounts = preparedStatement.executeBatch();
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
logStatements();
}
}
int totalCount = 0;
for (int i = 0; i < updateCounts.length; ++i) {
if (updateCounts[i] < 0)
throw new SQLException("Database returned invalid row count");
totalCount += updateCounts[i];
}
return totalCount;
} }
/** /**
@ -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();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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 не авторизован

View File

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

View File

@ -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 = 同步着时钟

View File

@ -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 хэш транзации

View File

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

View File

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

View File

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

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

View File

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