forked from Qortal/qortal
Merge branch 'master' into LTCv3-merge-test
This commit is contained in:
commit
fccb3a3f0c
@ -19,10 +19,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<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="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="REMOVE_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_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="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="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"/>
|
||||
|
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.8</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@ -3,13 +3,14 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.7</release>
|
||||
<release>1.3.8</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
</versions>
|
||||
<lastUpdated>20200812131412</lastUpdated>
|
||||
<lastUpdated>20200925114415</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
13
pom.xml
13
pom.xml
@ -3,20 +3,19 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.7</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<altcoinj.version>bf9fb80</altcoinj.version>
|
||||
<bitcoinj.version>0.15.6</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<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-text.version>1.8</commons-text.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.0-fixed</hsqldb.version>
|
||||
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.version>
|
||||
<jersey.version>2.29.1</jersey.version>
|
||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||
<log4j.version>2.12.1</log4j.version>
|
||||
@ -407,12 +406,6 @@
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>${hsqldb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>sqltool</artifactId>
|
||||
<version>${hsqldb-sqltool.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- CIYAM AT (automated transactions) -->
|
||||
<dependency>
|
||||
<groupId>org.ciyam</groupId>
|
||||
|
@ -5,10 +5,20 @@ import java.net.UnknownHostException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class Security {
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public abstract class Security {
|
||||
|
||||
public static final String API_KEY_HEADER = "X-API-KEY";
|
||||
|
||||
// TODO: replace with proper authentication
|
||||
public static void checkApiCallAllowed(HttpServletRequest request) {
|
||||
String expectedApiKey = Settings.getInstance().getApiKey();
|
||||
String passedApiKey = request.getHeader(API_KEY_HEADER);
|
||||
|
||||
if ((expectedApiKey != null && !expectedApiKey.equals(passedApiKey)) ||
|
||||
(passedApiKey != null && !passedApiKey.equals(expectedApiKey)))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
|
||||
InetAddress remoteAddr;
|
||||
try {
|
||||
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||
@ -19,4 +29,5 @@ public class Security {
|
||||
if (!remoteAddr.isLoopbackAddress())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ -13,17 +14,61 @@ import org.qortal.transaction.Transaction.TransactionType;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ActivitySummary {
|
||||
|
||||
public int blockCount;
|
||||
public int transactionCount;
|
||||
public int assetsIssued;
|
||||
public int namesRegistered;
|
||||
private int blockCount;
|
||||
private int assetsIssued;
|
||||
private int namesRegistered;
|
||||
|
||||
// Assuming TransactionType values are contiguous so 'length' equals count
|
||||
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
|
||||
public Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
||||
private Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
||||
private int totalTransactionCount = 0;
|
||||
|
||||
public ActivitySummary() {
|
||||
// Needed for JAXB
|
||||
}
|
||||
|
||||
public int getBlockCount() {
|
||||
return this.blockCount;
|
||||
}
|
||||
|
||||
public void setBlockCount(int blockCount) {
|
||||
this.blockCount = blockCount;
|
||||
}
|
||||
|
||||
public int getTotalTransactionCount() {
|
||||
return this.totalTransactionCount;
|
||||
}
|
||||
|
||||
public int getAssetsIssued() {
|
||||
return this.assetsIssued;
|
||||
}
|
||||
|
||||
public void setAssetsIssued(int assetsIssued) {
|
||||
this.assetsIssued = assetsIssued;
|
||||
}
|
||||
|
||||
public int getNamesRegistered() {
|
||||
return this.namesRegistered;
|
||||
}
|
||||
|
||||
public void setNamesRegistered(int namesRegistered) {
|
||||
this.namesRegistered = namesRegistered;
|
||||
}
|
||||
|
||||
public Map<TransactionType, Integer> getTransactionCountByType() {
|
||||
return Collections.unmodifiableMap(this.transactionCountByType);
|
||||
}
|
||||
|
||||
public void setTransactionCountByType(TransactionType transactionType, int transactionCount) {
|
||||
this.transactionCountByType.put(transactionType, transactionCount);
|
||||
|
||||
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
||||
public void setTransactionCountByType(Map<TransactionType, Integer> transactionCountByType) {
|
||||
this.transactionCountByType = new EnumMap<>(transactionCountByType);
|
||||
|
||||
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ public class NodeInfo {
|
||||
public String buildVersion;
|
||||
public long buildTimestamp;
|
||||
public String nodeId;
|
||||
public boolean isTestNet;
|
||||
|
||||
public NodeInfo() {
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@ -473,6 +474,7 @@ public class AddressesResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String computePublicize(String rawBytes58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
|
@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -40,7 +41,6 @@ import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.ActivitySummary;
|
||||
@ -57,6 +57,7 @@ import org.qortal.network.PeerAddress;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@ -118,6 +119,7 @@ public class AdminResource {
|
||||
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
|
||||
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
||||
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
||||
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
@ -132,6 +134,7 @@ public class AdminResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public NodeStatus status() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -152,6 +155,7 @@ public class AdminResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String shutdown() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -180,7 +184,10 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ActivitySummary summary() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
ActivitySummary summary = new ActivitySummary();
|
||||
|
||||
LocalDate date = LocalDate.now();
|
||||
@ -192,16 +199,13 @@ public class AdminResource {
|
||||
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
|
||||
int endHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
summary.blockCount = endHeight - startHeight;
|
||||
summary.setBlockCount(endHeight - startHeight);
|
||||
|
||||
summary.transactionCountByType = repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight);
|
||||
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
|
||||
|
||||
for (Integer count : summary.transactionCountByType.values())
|
||||
summary.transactionCount += count;
|
||||
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
|
||||
|
||||
summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size();
|
||||
|
||||
summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size();
|
||||
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
|
||||
|
||||
return summary;
|
||||
} catch (DataException e) {
|
||||
@ -209,6 +213,30 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/enginestats")
|
||||
@Operation(
|
||||
summary = "Fetch statistics snapshot for core engine",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = Controller.StatsSnapshot.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public Controller.StatsSnapshot getEngineStats() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
return Controller.getInstance().getStatsSnapshot();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/mintingaccounts")
|
||||
@Operation(
|
||||
@ -221,6 +249,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<MintingAccountData> getMintingAccounts() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -267,6 +296,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addMintingAccount(String seed58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -319,6 +349,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String deleteMintingAccount(String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -418,6 +449,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String orphan(String targetHeightString) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -435,8 +467,6 @@ public class AdminResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,6 +491,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String forceSync(String targetPeerAddress) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -492,8 +523,6 @@ public class AdminResource {
|
||||
return syncResult.name();
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (UnknownHostException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (InterruptedException e) {
|
||||
@ -501,4 +530,151 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/repository/data")
|
||||
@Operation(
|
||||
summary = "Export sensitive/node-local data from repository.",
|
||||
description = "Exports data to .script files on local machine"
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String exportRepository() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.exportNodeLocalData();
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform export
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/data")
|
||||
@Operation(
|
||||
summary = "Import data into repository.",
|
||||
description = "Imports data from file on local machine. Filename is forced to 'import.script' if apiKey is not set.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "MintingAccounts.script"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String importRepository(String filename) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
// Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
|
||||
if (Settings.getInstance().getApiKey() == null)
|
||||
filename = "import.script";
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.importDataFromFile(filename);
|
||||
repository.saveChanges();
|
||||
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform import
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/checkpoint")
|
||||
@Operation(
|
||||
summary = "Checkpoint data in repository.",
|
||||
description = "Forces repository to checkpoint uncommitted writes.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String checkpointRepository() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.checkpoint(true);
|
||||
repository.saveChanges();
|
||||
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform checkpoint
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/repository")
|
||||
@Operation(
|
||||
summary = "Perform maintenance on repository.",
|
||||
description = "Requires enough free space to rebuild repository. This will pause your node for a while."
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public void performRepositoryMaintenance() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.performPeriodicMaintenance();
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// No big deal
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.extensions.Extension;
|
||||
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import org.qortal.api.Security;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
|
||||
tags = {
|
||||
@ -30,5 +36,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
})
|
||||
}
|
||||
)
|
||||
@SecuritySchemes({
|
||||
@SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic"),
|
||||
@SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER)
|
||||
})
|
||||
public class ApiDefinition {
|
||||
}
|
@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
@ -156,6 +157,7 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildChat(ChatTransactionData transactionData) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -203,6 +205,7 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildChat(String rawBytes58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
|
@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -224,6 +225,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String cancelTrade(CrossChainCancelRequest cancelRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
|
@ -6,8 +6,11 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@ -26,10 +29,17 @@ import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.ConnectedPeer;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.PeerAddress;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@ -122,6 +132,7 @@ public class PeersResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -159,6 +170,7 @@ public class PeersResource {
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addPeer(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -213,6 +225,7 @@ public class PeersResource {
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removePeer(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -248,6 +261,7 @@ public class PeersResource {
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeKnownPeers(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@ -260,4 +274,68 @@ public class PeersResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/commonblock")
|
||||
@Operation(
|
||||
summary = "Report common block with given peer.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "node2.qortal.org"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the block",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockSummaryData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<BlockSummaryData> commonBlock(String targetPeerAddress) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
// Try to resolve passed address to make things easier
|
||||
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
|
||||
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
|
||||
|
||||
if (targetPeer == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int ourInitialHeight = Controller.getInstance().getChainHeight();
|
||||
boolean force = true;
|
||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||
|
||||
SynchronizationResult findCommonBlockResult = Synchronizer.getInstance().fetchSummariesFromCommonBlock(repository, targetPeer, ourInitialHeight, force, peerBlockSummaries);
|
||||
if (findCommonBlockResult != SynchronizationResult.OK)
|
||||
return null;
|
||||
|
||||
return peerBlockSummaries;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (UnknownHostException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (InterruptedException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
if (crossChainTradeData.mode == AcctMode.OFFERING)
|
||||
// We want when trade was created, not when it was last updated
|
||||
atStateTimestamp = atState.getCreation();
|
||||
atStateTimestamp = crossChainTradeData.creationTimestamp;
|
||||
else
|
||||
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||
|
||||
|
@ -51,16 +51,17 @@ public class AT {
|
||||
|
||||
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
|
||||
|
||||
byte[] codeHash = Crypto.digest(machineState.getCodeBytes());
|
||||
byte[] codeBytes = machineState.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash,
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
|
||||
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
|
||||
machineState.isFrozen(), machineState.getFrozenBalance());
|
||||
|
||||
byte[] stateData = machineState.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L, true);
|
||||
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@ -106,12 +107,11 @@ public class AT {
|
||||
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
|
||||
}
|
||||
|
||||
long creation = this.atData.getCreation();
|
||||
byte[] stateData = state.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
long atFees = api.calcFinalFees(state);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false);
|
||||
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
|
||||
|
||||
return api.getTransactions();
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import static java.util.stream.Collectors.toMap;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@ -15,6 +17,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
@ -791,15 +794,46 @@ public class Block {
|
||||
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
|
||||
}
|
||||
|
||||
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries) {
|
||||
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries, int maxHeight) {
|
||||
BigInteger cumulativeWeight = BigInteger.ZERO;
|
||||
int parentHeight = commonBlockHeight;
|
||||
byte[] parentBlockSignature = commonBlockSignature;
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE);
|
||||
|
||||
for (BlockSummaryData blockSummaryData : blockSummaries) {
|
||||
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT).add(calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData));
|
||||
StringBuilder stringBuilder = isLogging ? new StringBuilder(512) : null;
|
||||
|
||||
if (isLogging)
|
||||
stringBuilder.append(formatter.format(cumulativeWeight)).append(" -> ");
|
||||
|
||||
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT);
|
||||
if (isLogging)
|
||||
stringBuilder.append(formatter.format(cumulativeWeight)).append(" + ");
|
||||
|
||||
BigInteger blockWeight = calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
|
||||
if (isLogging)
|
||||
stringBuilder.append("(height: ")
|
||||
.append(parentHeight + 1)
|
||||
.append(", online: ")
|
||||
.append(blockSummaryData.getOnlineAccountsCount())
|
||||
.append(") ")
|
||||
.append(formatter.format(blockWeight));
|
||||
|
||||
cumulativeWeight = cumulativeWeight.add(blockWeight);
|
||||
if (isLogging)
|
||||
stringBuilder.append(" -> ").append(formatter.format(cumulativeWeight));
|
||||
|
||||
if (isLogging && blockSummaries.size() > 1)
|
||||
LOGGER.debug(() -> stringBuilder.toString()); //NOSONAR S1612 (false positive?)
|
||||
|
||||
parentHeight = blockSummaryData.getHeight();
|
||||
parentBlockSignature = blockSummaryData.getSignature();
|
||||
|
||||
/* Potential future consensus change: only comparing the same number of blocks.
|
||||
if (parentHeight >= maxHeight)
|
||||
break;
|
||||
*/
|
||||
}
|
||||
|
||||
return cumulativeWeight;
|
||||
|
@ -531,6 +531,7 @@ public class BlockChain {
|
||||
|
||||
private static void rebuildBlockchain() throws DataException {
|
||||
// (Re)build repository
|
||||
if (!RepositoryManager.wasPristineAtOpen())
|
||||
RepositoryManager.rebuild();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@ -567,7 +568,8 @@ public class BlockChain {
|
||||
--height;
|
||||
orphanBlockData = repository.getBlockRepository().fromHeight(height);
|
||||
|
||||
Controller.getInstance().onNewBlock(orphanBlockData);
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onOrphanedBlock(orphanBlockData);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
77
src/main/java/org/qortal/controller/AtStatesTrimmer.java
Normal file
77
src/main/java/org/qortal/controller/AtStatesTrimmer.java
Normal file
@ -0,0 +1,77 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class AtStatesTrimmer implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AtStatesTrimmer.class);
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("AT States trimmer");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
Thread.sleep(Settings.getInstance().getAtStatesTrimInterval());
|
||||
|
||||
BlockData chainTip = Controller.getInstance().getChainTip();
|
||||
if (chainTip == null || NTP.getTime() == null)
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
continue;
|
||||
|
||||
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
// We want to keep AT states near the tip of our copy of blockchain so we can process/orphan nearby blocks
|
||||
long chainTrimmableTimestamp = chainTip.getTimestamp() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
|
||||
long upperTrimmableTimestamp = Math.min(currentTrimmableTimestamp, chainTrimmableTimestamp);
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getAtStatesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
if (trimStartHeight >= upperTrimHeight)
|
||||
continue;
|
||||
|
||||
int numAtStatesTrimmed = repository.getATRepository().trimAtStates(trimStartHeight, upperTrimHeight, Settings.getInstance().getAtStatesTrimLimit());
|
||||
repository.saveChanges();
|
||||
|
||||
if (numAtStatesTrimmed > 0) {
|
||||
LOGGER.debug(() -> String.format("Trimmed %d AT state%s between blocks %d and %d",
|
||||
numAtStatesTrimmed, (numAtStatesTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping AT state trim height to %d", upperBatchHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to trim AT states: %s", e.getMessage()));
|
||||
} catch (InterruptedException e) {
|
||||
// Time to exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -306,8 +306,10 @@ public class BlockMinter extends Thread {
|
||||
}
|
||||
|
||||
if (newBlockMinted) {
|
||||
BlockData newBlockData = newBlock.getBlockData();
|
||||
// Notify Controller and broadcast our new chain to network
|
||||
BlockData newBlockData = newBlock.getBlockData();
|
||||
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(newBlockData);
|
||||
|
||||
Network network = Network.getInstance();
|
||||
|
@ -16,16 +16,24 @@ import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
@ -87,6 +95,7 @@ import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.DaemonThreadFactory;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@ -134,11 +143,24 @@ public class Controller extends Thread {
|
||||
private ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||
private volatile boolean notifyGroupMembershipChange = false;
|
||||
|
||||
private volatile BlockData chainTip = null;
|
||||
private static final int BLOCK_CACHE_SIZE = 10; // To cover typical Synchronizer request + a few spare
|
||||
/** Latest blocks on our chain. Note: tail/last is the latest block. */
|
||||
private final Deque<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 repositoryCheckpointTimestamp = startTime; // ms
|
||||
private long ntpCheckTimestamp = startTime; // ms
|
||||
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
||||
|
||||
private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
|
||||
|
||||
/** Whether we can mint new blocks, as reported by BlockMinter. */
|
||||
@ -181,6 +203,47 @@ public class Controller extends Thread {
|
||||
/** Cache of latest blocks' online accounts */
|
||||
Deque<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
|
||||
|
||||
private Controller(String[] args) {
|
||||
@ -236,21 +299,36 @@ public class Controller extends Thread {
|
||||
|
||||
/** Returns current blockchain height, or 0 if it's not available. */
|
||||
public int getChainHeight() {
|
||||
BlockData blockData = this.chainTip;
|
||||
synchronized (this.latestBlocks) {
|
||||
BlockData blockData = this.latestBlocks.peekLast();
|
||||
if (blockData == null)
|
||||
return 0;
|
||||
|
||||
return blockData.getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns highest block, or null if it's not available. */
|
||||
public BlockData getChainTip() {
|
||||
return this.chainTip;
|
||||
synchronized (this.latestBlocks) {
|
||||
return this.latestBlocks.peekLast();
|
||||
}
|
||||
}
|
||||
|
||||
/** Cache new blockchain tip. */
|
||||
public void setChainTip(BlockData blockData) {
|
||||
this.chainTip = blockData;
|
||||
public void refillLatestBlocksCache() throws DataException {
|
||||
// Set initial chain height/tip
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
synchronized (this.latestBlocks) {
|
||||
this.latestBlocks.clear();
|
||||
|
||||
for (int i = 0; i < BLOCK_CACHE_SIZE && blockData != null; ++i) {
|
||||
this.latestBlocks.addFirst(blockData);
|
||||
blockData = repository.getBlockRepository().fromHeight(blockData.getHeight() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ReentrantLock getBlockchainLock() {
|
||||
@ -332,13 +410,8 @@ public class Controller extends Thread {
|
||||
try {
|
||||
BlockChain.validate();
|
||||
|
||||
// Set initial chain height/tip
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
Controller.getInstance().setChainTip(blockData);
|
||||
LOGGER.info(String.format("Our chain height at start-up: %d", blockData.getHeight()));
|
||||
}
|
||||
Controller.getInstance().refillLatestBlocksCache();
|
||||
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't validate blockchain", e);
|
||||
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
||||
@ -413,6 +486,11 @@ public class Controller extends Thread {
|
||||
Thread.currentThread().setName("Controller");
|
||||
|
||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
||||
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
||||
|
||||
ExecutorService trimExecutor = Executors.newCachedThreadPool(new DaemonThreadFactory());
|
||||
trimExecutor.execute(new AtStatesTrimmer());
|
||||
trimExecutor.execute(new OnlineAccountsSignaturesTrimmer());
|
||||
|
||||
try {
|
||||
while (!isStopping) {
|
||||
@ -454,6 +532,18 @@ public class Controller extends Thread {
|
||||
final long requestMinimumTimestamp = now - ARBITRARY_REQUEST_TIMEOUT;
|
||||
arbitraryDataRequests.entrySet().removeIf(entry -> entry.getValue().getC() < requestMinimumTimestamp);
|
||||
|
||||
// Time to 'checkpoint' uncommitted repository writes?
|
||||
if (now >= repositoryCheckpointTimestamp + repositoryCheckpointInterval) {
|
||||
repositoryCheckpointTimestamp = now + repositoryCheckpointInterval;
|
||||
|
||||
if (Settings.getInstance().getShowCheckpointNotification())
|
||||
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "DB_CHECKPOINT"),
|
||||
Translator.INSTANCE.translate("SysTray", "PERFORMING_DB_CHECKPOINT"),
|
||||
MessageType.INFO);
|
||||
|
||||
RepositoryManager.checkpoint(true);
|
||||
}
|
||||
|
||||
// Give repository a chance to backup (if enabled)
|
||||
if (repositoryBackupInterval > 0 && now >= repositoryBackupTimestamp + repositoryBackupInterval) {
|
||||
repositoryBackupTimestamp = now + repositoryBackupInterval;
|
||||
@ -486,7 +576,17 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Clear interrupted flag so we can shutdown trim threads
|
||||
Thread.interrupted();
|
||||
// Fall-through to exit
|
||||
} finally {
|
||||
trimExecutor.shutdownNow();
|
||||
|
||||
try {
|
||||
trimExecutor.awaitTermination(2L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// We tried...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,9 +656,10 @@ public class Controller extends Thread {
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
boolean hasStatusChanged = false;
|
||||
BlockData priorChainTip = this.getChainTip();
|
||||
|
||||
synchronized (this.syncLock) {
|
||||
this.syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (this.syncPercent < 100) {
|
||||
@ -570,8 +671,6 @@ public class Controller extends Thread {
|
||||
if (hasStatusChanged)
|
||||
updateSysTray();
|
||||
|
||||
BlockData priorChainTip = this.chainTip;
|
||||
|
||||
try {
|
||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||
switch (syncResult) {
|
||||
@ -640,9 +739,6 @@ public class Controller extends Thread {
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
|
||||
// Update chain-tip, systray, notify peers, websockets, etc.
|
||||
this.onNewBlock(newChainTip);
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
||||
}
|
||||
@ -827,15 +923,109 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when we've received a new block.
|
||||
* <p>
|
||||
* See <b>WARNING</b> for {@link EventBus#notify(Event)}
|
||||
* to prevent deadlocks.
|
||||
*/
|
||||
public void onNewBlock(BlockData latestBlockData) {
|
||||
// Protective copy
|
||||
BlockData blockDataCopy = new BlockData(latestBlockData);
|
||||
|
||||
this.setChainTip(blockDataCopy);
|
||||
synchronized (this.latestBlocks) {
|
||||
BlockData cachedChainTip = this.latestBlocks.peekLast();
|
||||
|
||||
if (cachedChainTip != null && Arrays.equals(cachedChainTip.getSignature(), blockDataCopy.getReference())) {
|
||||
// Chain tip is parent for new latest block, so we can safely add new latest block
|
||||
this.latestBlocks.addLast(latestBlockData);
|
||||
|
||||
// Trim if necessary
|
||||
if (this.latestBlocks.size() >= BLOCK_CACHE_SIZE)
|
||||
this.latestBlocks.pollFirst();
|
||||
} else {
|
||||
if (cachedChainTip != null)
|
||||
// Chain tip didn't match - potentially abnormal behaviour?
|
||||
LOGGER.debug(() -> String.format("Cached chain tip %.8s not parent for new latest block %.8s (reference %.8s)",
|
||||
Base58.encode(cachedChainTip.getSignature()),
|
||||
Base58.encode(blockDataCopy.getSignature()),
|
||||
Base58.encode(blockDataCopy.getReference())));
|
||||
|
||||
// Defensively rebuild cache
|
||||
try {
|
||||
this.stats.latestBlocksCacheRefills.incrementAndGet();
|
||||
|
||||
this.refillLatestBlocksCache();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> "Couldn't refill latest blocks cache?", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onNewOrOrphanedBlock(blockDataCopy, NewBlockEvent::new);
|
||||
}
|
||||
|
||||
public static class OrphanedBlockEvent implements Event {
|
||||
private final BlockData blockData;
|
||||
|
||||
public OrphanedBlockEvent(BlockData blockData) {
|
||||
this.blockData = blockData;
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
return this.blockData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when we've orphaned a block.
|
||||
* <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;
|
||||
|
||||
// Notify listeners, trade-bot, etc.
|
||||
EventBus.INSTANCE.notify(new NewBlockEvent(blockDataCopy));
|
||||
EventBus.INSTANCE.notify(eventConstructor.apply(blockDataCopy));
|
||||
|
||||
if (this.notifyGroupMembershipChange) {
|
||||
this.notifyGroupMembershipChange = false;
|
||||
@ -935,11 +1125,31 @@ public class Controller extends Thread {
|
||||
private void onNetworkGetBlockMessage(Peer peer, Message message) {
|
||||
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
|
||||
byte[] signature = getBlockMessage.getSignature();
|
||||
this.stats.getBlockMessageStats.requests.incrementAndGet();
|
||||
|
||||
ByteArray signatureAsByteArray = new ByteArray(signature);
|
||||
|
||||
BlockMessage cachedBlockMessage = this.blockMessageCache.get(signatureAsByteArray);
|
||||
|
||||
// Check cached latest block message
|
||||
if (cachedBlockMessage != null) {
|
||||
this.stats.getBlockMessageStats.cacheHits.incrementAndGet();
|
||||
|
||||
// We need to duplicate it to prevent multiple threads setting ID on the same message
|
||||
BlockMessage clonedBlockMessage = cachedBlockMessage.cloneWithNewId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(clonedBlockMessage))
|
||||
peer.disconnect("failed to send block");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
|
||||
if (blockData == null) {
|
||||
// We don't have this block
|
||||
this.stats.getBlockMessageStats.unknownBlocks.getAndIncrement();
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
|
||||
@ -954,10 +1164,19 @@ public class Controller extends Thread {
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
|
||||
Message blockMessage = new BlockMessage(block);
|
||||
BlockMessage blockMessage = new BlockMessage(block);
|
||||
blockMessage.setId(message.getId());
|
||||
|
||||
// This call also causes the other needed data to be pulled in from repository
|
||||
if (!peer.sendMessage(blockMessage))
|
||||
peer.disconnect("failed to send block");
|
||||
|
||||
// If request is for a recent block, cache it
|
||||
if (getChainHeight() - blockData.getHeight() <= BLOCK_CACHE_SIZE) {
|
||||
this.stats.getBlockMessageStats.cacheFills.incrementAndGet();
|
||||
|
||||
this.blockMessageCache.put(new ByteArray(blockData.getSignature()), blockMessage);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send block %s to peer %s", Base58.encode(signature), peer), e);
|
||||
}
|
||||
@ -1004,59 +1223,110 @@ public class Controller extends Thread {
|
||||
|
||||
private void onNetworkGetBlockSummariesMessage(Peer peer, Message message) {
|
||||
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
|
||||
byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
|
||||
final byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
|
||||
this.stats.getBlockSummariesStats.requests.incrementAndGet();
|
||||
|
||||
// If peer's parent signature matches our latest block signature
|
||||
// then we can short-circuit with an empty response
|
||||
BlockData chainTip = getChainTip();
|
||||
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
||||
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
blockSummariesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(blockSummariesMessage))
|
||||
peer.disconnect("failed to send block summaries");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
// 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());
|
||||
|
||||
do {
|
||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||
|
||||
if (blockData == null)
|
||||
// No more blocks to send to peer
|
||||
break;
|
||||
|
||||
while (blockData != null && blockSummaries.size() < numberRequested) {
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
|
||||
blockSummaries.add(blockSummary);
|
||||
parentSignature = blockData.getSignature();
|
||||
} while (blockSummaries.size() < numberRequested);
|
||||
|
||||
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
}
|
||||
} else {
|
||||
this.stats.getBlockSummariesStats.cacheHits.incrementAndGet();
|
||||
|
||||
if (blockSummaries.size() >= getBlockSummariesMessage.getNumberRequested())
|
||||
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
|
||||
}
|
||||
|
||||
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
|
||||
blockSummariesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(blockSummariesMessage))
|
||||
peer.disconnect("failed to send block summaries");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) {
|
||||
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
|
||||
byte[] parentSignature = getSignaturesMessage.getParentSignature();
|
||||
final byte[] parentSignature = getSignaturesMessage.getParentSignature();
|
||||
this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet();
|
||||
|
||||
// If peer's parent signature matches our latest block signature
|
||||
// then we can short-circuit with an empty response
|
||||
BlockData chainTip = getChainTip();
|
||||
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
||||
Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
|
||||
signaturesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(signaturesMessage))
|
||||
peer.disconnect("failed to send signatures (v2)");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
|
||||
do {
|
||||
// Attempt to serve from our cache of latest blocks
|
||||
synchronized (this.latestBlocks) {
|
||||
signatures = this.latestBlocks.stream()
|
||||
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
|
||||
.map(BlockData::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (signatures.isEmpty()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int numberRequested = getSignaturesMessage.getNumberRequested();
|
||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||
|
||||
if (blockData == null)
|
||||
// No more signatures to send to peer
|
||||
break;
|
||||
while (blockData != null && signatures.size() < numberRequested) {
|
||||
signatures.add(blockData.getSignature());
|
||||
|
||||
parentSignature = blockData.getSignature();
|
||||
signatures.add(parentSignature);
|
||||
} while (signatures.size() < getSignaturesMessage.getNumberRequested());
|
||||
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)");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkHeightV2Message(Peer peer, Message message) {
|
||||
@ -1406,26 +1676,6 @@ public class Controller extends Thread {
|
||||
|
||||
// Refresh our online accounts signatures?
|
||||
sendOurOnlineAccountsInfo();
|
||||
|
||||
// Trim blockchain by removing 'old' online accounts signatures
|
||||
long upperMintedTimestamp = now - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
||||
trimOldOnlineAccountsSignatures(upperMintedTimestamp);
|
||||
}
|
||||
|
||||
private void trimOldOnlineAccountsSignatures(long upperMintedTimestamp) {
|
||||
try (final Repository repository = RepositoryManager.tryRepository()) {
|
||||
if (repository == null)
|
||||
return;
|
||||
|
||||
int numBlocksTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(upperMintedTimestamp);
|
||||
|
||||
if (numBlocksTrimmed > 0)
|
||||
LOGGER.debug(() -> String.format("Trimmed old online accounts signatures from %d block%s", numBlocksTrimmed, (numBlocksTrimmed != 1 ? "s" : "")));
|
||||
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to trim old online accounts signatures: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendOurOnlineAccountsInfo() {
|
||||
@ -1687,4 +1937,8 @@ public class Controller extends Thread {
|
||||
return now - offset;
|
||||
}
|
||||
|
||||
public StatsSnapshot getStatsSnapshot() {
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OnlineAccountsSignaturesTrimmer.class);
|
||||
|
||||
private static final long INITIAL_SLEEP_PERIOD = 5 * 60 * 1000L + 1234L; // ms
|
||||
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Online Accounts trimmer");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Don't even start trimming until initial rush has ended
|
||||
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
Thread.sleep(Settings.getInstance().getOnlineSignaturesTrimInterval());
|
||||
|
||||
BlockData chainTip = Controller.getInstance().getChainTip();
|
||||
if (chainTip == null || NTP.getTime() == null)
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
continue;
|
||||
|
||||
// Trim blockchain by removing 'old' online accounts signatures
|
||||
long upperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getOnlineSignaturesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
if (trimStartHeight >= upperTrimHeight)
|
||||
continue;
|
||||
|
||||
int numSigsTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(trimStartHeight, upperTrimHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
if (numSigsTrimmed > 0) {
|
||||
LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
|
||||
numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to trim online accounts signatures: %s", e.getMessage()));
|
||||
} catch (InterruptedException e) {
|
||||
// Time to exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -175,7 +175,7 @@ public class Synchronizer {
|
||||
* @throws DataException
|
||||
* @throws 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
|
||||
// Failing that, back off exponentially
|
||||
int step = INITIAL_BLOCK_STEP;
|
||||
@ -313,18 +313,21 @@ public class Synchronizer {
|
||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
|
||||
|
||||
// Populate minter account levels for both lists of block summaries
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
||||
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
||||
|
||||
final int mutualHeight = commonBlockHeight - 1 + Math.min(ourBlockSummaries.size(), peerBlockSummaries.size());
|
||||
|
||||
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries, mutualHeight);
|
||||
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
|
||||
// If our blockchain has greater weight then don't synchronize with peer
|
||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
return SynchronizationResult.INFERIOR_CHAIN;
|
||||
}
|
||||
}
|
||||
@ -405,12 +408,15 @@ public class Synchronizer {
|
||||
Block block = new Block(repository, orphanBlockData);
|
||||
block.orphan();
|
||||
|
||||
LOGGER.trace(String.format("Orphaned block height %d, sig %.8s", ourHeight, Base58.encode(orphanBlockData.getSignature())));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
--ourHeight;
|
||||
orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
|
||||
Controller.getInstance().onNewBlock(orphanBlockData);
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onOrphanedBlock(orphanBlockData);
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
|
||||
@ -431,6 +437,8 @@ public class Synchronizer {
|
||||
|
||||
newBlock.process();
|
||||
|
||||
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
@ -513,6 +521,8 @@ public class Synchronizer {
|
||||
|
||||
newBlock.process();
|
||||
|
||||
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
|
@ -610,38 +610,26 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -649,7 +637,7 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
|
||||
tradeData.qortalAtAddress = atAddress;
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = atStateData.getCreation();
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
@ -561,38 +561,26 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -600,7 +588,7 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
|
||||
tradeData.qortalAtAddress = atAddress;
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = atStateData.getCreation();
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
@ -42,6 +43,27 @@ public abstract class Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 32-byte SHA-256 digest of message passed in input.
|
||||
*
|
||||
* @param input
|
||||
* variable-length byte[] message
|
||||
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
|
||||
*/
|
||||
public static byte[] digest(ByteBuffer input) {
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
// SHA2-256
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
sha256.update(input);
|
||||
return sha256.digest();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 message digest not available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 32-byte digest of two rounds of SHA-256 on message passed in input.
|
||||
*
|
||||
|
@ -5,7 +5,6 @@ public class ATStateData {
|
||||
// Properties
|
||||
private String ATAddress;
|
||||
private Integer height;
|
||||
private Long creation;
|
||||
private byte[] stateData;
|
||||
private byte[] stateHash;
|
||||
private Long fees;
|
||||
@ -14,10 +13,9 @@ public class ATStateData {
|
||||
// Constructors
|
||||
|
||||
/** Create new ATStateData */
|
||||
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
this.ATAddress = ATAddress;
|
||||
this.height = height;
|
||||
this.creation = creation;
|
||||
this.stateData = stateData;
|
||||
this.stateHash = stateHash;
|
||||
this.fees = fees;
|
||||
@ -26,21 +24,21 @@ public class ATStateData {
|
||||
|
||||
/** For recreating per-block ATStateData from repository where not all info is needed */
|
||||
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
this(ATAddress, height, null, null, stateHash, fees, isInitial);
|
||||
this(ATAddress, height, null, stateHash, fees, isInitial);
|
||||
}
|
||||
|
||||
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||
public ATStateData(String ATAddress, byte[] stateHash) {
|
||||
// This won't ever be initial AT state from deployment as that's never serialized over the network,
|
||||
// but generated when the DeployAtTransaction is processed locally.
|
||||
this(ATAddress, null, null, null, stateHash, null, false);
|
||||
this(ATAddress, null, null, stateHash, null, false);
|
||||
}
|
||||
|
||||
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
|
||||
// This won't ever be initial AT state from deployment as that's never serialized over the network,
|
||||
// but generated when the DeployAtTransaction is processed locally.
|
||||
this(ATAddress, null, null, null, stateHash, fees, false);
|
||||
this(ATAddress, null, null, stateHash, fees, false);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@ -58,10 +56,6 @@ public class ATStateData {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Long getCreation() {
|
||||
return this.creation;
|
||||
}
|
||||
|
||||
public byte[] getStateData() {
|
||||
return this.stateData;
|
||||
}
|
||||
|
@ -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) {
|
||||
List<Listener> clonedListeners;
|
||||
|
||||
|
@ -10,12 +10,12 @@ import java.util.Set;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public enum Translator {
|
||||
INSTANCE;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
|
||||
private static final String DEFAULT_LANG = Locale.getDefault().getLanguage();
|
||||
|
||||
private static final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
|
||||
|
||||
@ -34,7 +34,7 @@ public enum Translator {
|
||||
}
|
||||
|
||||
public String translate(String className, String key) {
|
||||
return this.translate(className, DEFAULT_LANG, key);
|
||||
return this.translate(className, Settings.getInstance().getLocaleLang(), key);
|
||||
}
|
||||
|
||||
public Set<String> keySet(String className, String lang) {
|
||||
|
@ -33,6 +33,7 @@ import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.google.common.net.InetAddresses;
|
||||
|
||||
@ -348,21 +349,37 @@ public class Peer {
|
||||
if (this.byteBuffer == null)
|
||||
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
|
||||
|
||||
final int priorPosition = this.byteBuffer.position();
|
||||
final int bytesRead = this.socketChannel.read(this.byteBuffer);
|
||||
if (bytesRead == -1) {
|
||||
this.disconnect("EOF");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Received %d bytes from peer %s", bytesRead, this));
|
||||
LOGGER.trace(() -> {
|
||||
if (bytesRead > 0) {
|
||||
byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
|
||||
this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
|
||||
String leadingHex = HashCode.fromBytes(leadingBytes).toString();
|
||||
|
||||
return String.format("Received %d bytes, starting %s, into byteBuffer[%d] from peer %s",
|
||||
bytesRead,
|
||||
leadingHex,
|
||||
priorPosition,
|
||||
this);
|
||||
} else {
|
||||
return String.format("Received %d bytes into byteBuffer[%d] from peer %s", bytesRead, priorPosition, this);
|
||||
}
|
||||
});
|
||||
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
|
||||
|
||||
while (true) {
|
||||
final Message message;
|
||||
|
||||
// Can we build a message from buffer now?
|
||||
ByteBuffer readOnlyBuffer = this.byteBuffer.asReadOnlyBuffer().flip();
|
||||
try {
|
||||
message = Message.fromByteBuffer(this.byteBuffer);
|
||||
message = Message.fromByteBuffer(readOnlyBuffer);
|
||||
} catch (MessageException e) {
|
||||
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
|
||||
this.disconnect(e.getMessage());
|
||||
@ -387,6 +404,13 @@ public class Peer {
|
||||
|
||||
LOGGER.trace(() -> String.format("Received %s message with ID %d from peer %s", message.getType().name(), message.getId(), this));
|
||||
|
||||
// Tidy up buffers:
|
||||
this.byteBuffer.flip();
|
||||
// Read-only, flipped buffer's position will be after end of message, so copy that
|
||||
this.byteBuffer.position(readOnlyBuffer.position());
|
||||
// Copy bytes after read message to front of buffer, adjusting position accordingly, reset limit to capacity
|
||||
this.byteBuffer.compact();
|
||||
|
||||
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
|
||||
if (queue != null) {
|
||||
// Adding message to queue will unblock thread waiting for response
|
||||
@ -399,7 +423,7 @@ public class Peer {
|
||||
|
||||
// Add message to pending queue
|
||||
if (!this.pendingMessages.offer(message)) {
|
||||
LOGGER.info(String.format("No room to queue message from peer %s - discarding", this));
|
||||
LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -454,10 +478,24 @@ public class Peer {
|
||||
while (outputBuffer.hasRemaining()) {
|
||||
int bytesWritten = this.socketChannel.write(outputBuffer);
|
||||
|
||||
LOGGER.trace(() -> String.format("Sent %d bytes of %s message with ID %d to peer %s",
|
||||
bytesWritten,
|
||||
message.getType().name(),
|
||||
message.getId(),
|
||||
this));
|
||||
|
||||
if (bytesWritten == 0)
|
||||
// Underlying socket's internal buffer probably full,
|
||||
// so wait a short while for bytes to actually be transmitted over the wire
|
||||
this.socketChannel.wait(1L);
|
||||
|
||||
/*
|
||||
* NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
|
||||
* as this releases the lock held by synchronized() above
|
||||
* and would allow another thread to send another message,
|
||||
* potentially interleaving them on-the-wire, causing checksum failures
|
||||
* and connection loss.
|
||||
*/
|
||||
Thread.sleep(1L); //NOSONAR squid:S2276
|
||||
}
|
||||
}
|
||||
} catch (MessageException e) {
|
||||
|
@ -34,6 +34,7 @@ public class BlockMessage extends Message {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = block;
|
||||
this.blockData = block.getBlockData();
|
||||
this.height = block.getBlockData().getHeight();
|
||||
}
|
||||
|
||||
@ -93,4 +94,10 @@ public class BlockMessage extends Message {
|
||||
}
|
||||
}
|
||||
|
||||
public BlockMessage cloneWithNewId(int newId) {
|
||||
BlockMessage clone = new BlockMessage(this.block);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -160,80 +160,72 @@ public abstract class Message {
|
||||
/**
|
||||
* Attempt to read a message from byte buffer.
|
||||
*
|
||||
* @param byteBuffer
|
||||
* @param readOnlyBuffer
|
||||
* @return null if no complete message can be read
|
||||
* @throws MessageException
|
||||
*/
|
||||
public static Message fromByteBuffer(ByteBuffer byteBuffer) throws MessageException {
|
||||
public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
|
||||
try {
|
||||
byteBuffer.flip();
|
||||
|
||||
ByteBuffer readBuffer = byteBuffer.asReadOnlyBuffer();
|
||||
|
||||
// Read only enough bytes to cover Message "magic" preamble
|
||||
byte[] messageMagic = new byte[MAGIC_LENGTH];
|
||||
readBuffer.get(messageMagic);
|
||||
readOnlyBuffer.get(messageMagic);
|
||||
|
||||
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
|
||||
// Didn't receive correct Message "magic"
|
||||
throw new MessageException("Received incorrect message 'magic'");
|
||||
|
||||
// Find supporting object
|
||||
int typeValue = readBuffer.getInt();
|
||||
int typeValue = readOnlyBuffer.getInt();
|
||||
MessageType messageType = MessageType.valueOf(typeValue);
|
||||
if (messageType == null)
|
||||
// Unrecognised message type
|
||||
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
|
||||
|
||||
// Optional message ID
|
||||
byte hasId = readBuffer.get();
|
||||
byte hasId = readOnlyBuffer.get();
|
||||
int id = -1;
|
||||
if (hasId != 0) {
|
||||
id = readBuffer.getInt();
|
||||
id = readOnlyBuffer.getInt();
|
||||
|
||||
if (id <= 0)
|
||||
// Invalid ID
|
||||
throw new MessageException("Invalid negative ID");
|
||||
}
|
||||
|
||||
int dataSize = readBuffer.getInt();
|
||||
int dataSize = readOnlyBuffer.getInt();
|
||||
|
||||
if (dataSize > MAX_DATA_SIZE)
|
||||
// Too large
|
||||
throw new MessageException(String.format("Declared data length %d larger than max allowed %d", dataSize, MAX_DATA_SIZE));
|
||||
|
||||
// Don't have all the data yet?
|
||||
if (dataSize > 0 && dataSize + CHECKSUM_LENGTH > readOnlyBuffer.remaining())
|
||||
return null;
|
||||
|
||||
ByteBuffer dataSlice = null;
|
||||
if (dataSize > 0) {
|
||||
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
|
||||
readBuffer.get(expectedChecksum);
|
||||
readOnlyBuffer.get(expectedChecksum);
|
||||
|
||||
// Remember this position in readBuffer so we can pass to Message subclass
|
||||
dataSlice = readBuffer.slice();
|
||||
|
||||
// Consume data from buffer
|
||||
byte[] data = new byte[dataSize];
|
||||
readBuffer.get(data);
|
||||
|
||||
// We successfully read all the data bytes, so we can set limit on dataSlice
|
||||
// Slice data in readBuffer so we can pass to Message subclass
|
||||
dataSlice = readOnlyBuffer.slice();
|
||||
dataSlice.limit(dataSize);
|
||||
|
||||
// Test checksum
|
||||
byte[] actualChecksum = generateChecksum(data);
|
||||
byte[] actualChecksum = generateChecksum(dataSlice);
|
||||
if (!Arrays.equals(expectedChecksum, actualChecksum))
|
||||
throw new MessageException("Message checksum incorrect");
|
||||
|
||||
// Reset position after being consumed by generateChecksum
|
||||
dataSlice.position(0);
|
||||
// Update position in readOnlyBuffer
|
||||
readOnlyBuffer.position(readOnlyBuffer.position() + dataSize);
|
||||
}
|
||||
|
||||
Message message = messageType.fromByteBuffer(id, dataSlice);
|
||||
|
||||
// We successfully read a message, so bump byteBuffer's position to reflect this
|
||||
byteBuffer.position(readBuffer.position());
|
||||
|
||||
return message;
|
||||
return messageType.fromByteBuffer(id, dataSlice);
|
||||
} catch (BufferUnderflowException e) {
|
||||
// Not enough bytes to fully decode message...
|
||||
return null;
|
||||
} finally {
|
||||
byteBuffer.compact();
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,6 +233,10 @@ public abstract class Message {
|
||||
return Arrays.copyOfRange(Crypto.digest(data), 0, CHECKSUM_LENGTH);
|
||||
}
|
||||
|
||||
protected static byte[] generateChecksum(ByteBuffer dataBuffer) {
|
||||
return Arrays.copyOfRange(Crypto.digest(dataBuffer), 0, CHECKSUM_LENGTH);
|
||||
}
|
||||
|
||||
public byte[] toBytes() throws MessageException {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
|
||||
|
@ -87,6 +87,21 @@ public interface ATRepository {
|
||||
*/
|
||||
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.
|
||||
* <p>
|
||||
|
@ -143,13 +143,21 @@ public interface BlockRepository {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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.
|
||||
|
@ -47,6 +47,12 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public void backup(boolean quick) throws DataException;
|
||||
|
||||
public void checkpoint(boolean quick) throws DataException;
|
||||
|
||||
public void performPeriodicMaintenance() throws DataException;
|
||||
|
||||
public void exportNodeLocalData() throws DataException;
|
||||
|
||||
public void importDataFromFile(String filename) throws DataException;
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package org.qortal.repository;
|
||||
|
||||
public interface RepositoryFactory {
|
||||
|
||||
public boolean wasPristineAtOpen();
|
||||
|
||||
public RepositoryFactory reopen() throws DataException;
|
||||
|
||||
public Repository getRepository() throws DataException;
|
||||
|
@ -8,6 +8,13 @@ public abstract class RepositoryManager {
|
||||
repositoryFactory = newRepositoryFactory;
|
||||
}
|
||||
|
||||
public static boolean wasPristineAtOpen() throws DataException {
|
||||
if (repositoryFactory == null)
|
||||
throw new DataException("No repository available");
|
||||
|
||||
return repositoryFactory.wasPristineAtOpen();
|
||||
}
|
||||
|
||||
public static Repository getRepository() throws DataException {
|
||||
if (repositoryFactory == null)
|
||||
throw new DataException("No repository available");
|
||||
@ -35,6 +42,14 @@ public abstract class RepositoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkpoint(boolean quick) {
|
||||
try (final Repository repository = getRepository()) {
|
||||
repository.checkpoint(quick);
|
||||
} catch (DataException e) {
|
||||
// Checkpoint is best-effort so don't complain
|
||||
}
|
||||
}
|
||||
|
||||
public static void rebuild() throws DataException {
|
||||
RepositoryFactory oldRepositoryFactory = repositoryFactory;
|
||||
|
||||
|
@ -50,7 +50,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
boolean hadFatalError = resultSet.getBoolean(10);
|
||||
boolean isFrozen = resultSet.getBoolean(11);
|
||||
|
||||
Long frozenBalance = resultSet.getLong(11);
|
||||
Long frozenBalance = resultSet.getLong(12);
|
||||
if (frozenBalance == 0 && resultSet.wasNull())
|
||||
frozenBalance = null;
|
||||
|
||||
@ -118,7 +118,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
boolean hadFatalError = resultSet.getBoolean(10);
|
||||
boolean isFrozen = resultSet.getBoolean(11);
|
||||
|
||||
Long frozenBalance = resultSet.getLong(11);
|
||||
Long frozenBalance = resultSet.getLong(12);
|
||||
if (frozenBalance == 0 && resultSet.wasNull())
|
||||
frozenBalance = null;
|
||||
|
||||
@ -147,7 +147,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
bindParams.add(codeHash);
|
||||
|
||||
if (isExecutable != null) {
|
||||
sql.append("AND is_finished = ? ");
|
||||
sql.append("AND is_finished != ? ");
|
||||
bindParams.add(isExecutable);
|
||||
}
|
||||
|
||||
@ -248,22 +248,22 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
|
||||
String sql = "SELECT created_when, state_data, state_hash, fees, is_initial "
|
||||
String sql = "SELECT state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE AT_address = ? AND height = ? "
|
||||
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
|
||||
+ "LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
long created = resultSet.getLong(1);
|
||||
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(3);
|
||||
long fees = resultSet.getLong(4);
|
||||
boolean isInitial = resultSet.getBoolean(5);
|
||||
byte[] stateData = resultSet.getBytes(1); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(2);
|
||||
long fees = resultSet.getLong(3);
|
||||
boolean isInitial = resultSet.getBoolean(4);
|
||||
|
||||
return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT state from repository", e);
|
||||
}
|
||||
@ -271,12 +271,13 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public ATStateData getLatestATState(String atAddress) throws DataException {
|
||||
String sql = "SELECT height, created_when, state_data, state_hash, fees, is_initial "
|
||||
String sql = "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE AT_address = ? "
|
||||
// AT_address then height so the compound primary key is used as an index
|
||||
// Both must be the same direction also
|
||||
+ "ORDER BY AT_address DESC, height DESC "
|
||||
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ? "
|
||||
// Order by AT_address and height to use compound primary key as index
|
||||
// Both must be the same direction (DESC) also
|
||||
+ "ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||
+ "LIMIT 1 ";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
||||
@ -284,13 +285,12 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
return null;
|
||||
|
||||
int height = resultSet.getInt(1);
|
||||
long created = resultSet.getLong(2);
|
||||
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(4);
|
||||
long fees = resultSet.getLong(5);
|
||||
boolean isInitial = resultSet.getBoolean(6);
|
||||
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(3);
|
||||
long fees = resultSet.getLong(4);
|
||||
boolean isInitial = resultSet.getBoolean(5);
|
||||
|
||||
return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch latest AT state from repository", e);
|
||||
}
|
||||
@ -303,21 +303,22 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
List<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 "
|
||||
+ "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 "
|
||||
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address ");
|
||||
|
||||
if (minimumFinalHeight != null) {
|
||||
sql.append("AND height >= ? ");
|
||||
sql.append("AND ATStates.height >= ? ");
|
||||
bindParams.add(minimumFinalHeight);
|
||||
}
|
||||
|
||||
// AT_address then height so the compound primary key is used as an index
|
||||
// Both must be the same direction also
|
||||
sql.append("ORDER BY AT_address DESC, height DESC "
|
||||
// Order by AT_address and height to use compound primary key as index
|
||||
// Both must be the same direction (DESC) also
|
||||
sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||
+ "LIMIT 1 "
|
||||
+ ") AS FinalATStates "
|
||||
+ "WHERE code_hash = ? ");
|
||||
@ -339,7 +340,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
bindParams.add(rawExpectedValue);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY height ");
|
||||
sql.append(" ORDER BY FinalATStates.height ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC");
|
||||
|
||||
@ -354,13 +355,12 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
do {
|
||||
String atAddress = resultSet.getString(1);
|
||||
int height = resultSet.getInt(2);
|
||||
long created = resultSet.getLong(3);
|
||||
byte[] stateData = resultSet.getBytes(4); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(5);
|
||||
long fees = resultSet.getLong(6);
|
||||
boolean isInitial = resultSet.getBoolean(7);
|
||||
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(4);
|
||||
long fees = resultSet.getLong(5);
|
||||
boolean isInitial = resultSet.getBoolean(6);
|
||||
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
|
||||
atStates.add(atStateData);
|
||||
} while (resultSet.next());
|
||||
@ -374,8 +374,10 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
@Override
|
||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
||||
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE height = ? "
|
||||
+ "FROM ATs "
|
||||
+ "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";
|
||||
|
||||
List<ATStateData> atStates = new ArrayList<>();
|
||||
@ -402,29 +404,131 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ATStateData atStateData) throws DataException {
|
||||
// We shouldn't ever save partial ATStateData
|
||||
if (atStateData.getCreation() == null || atStateData.getStateHash() == null || atStateData.getHeight() == null)
|
||||
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
|
||||
public int getAtTrimHeight() throws DataException {
|
||||
String sql = "SELECT AT_trim_height FROM DatabaseInfo";
|
||||
|
||||
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())
|
||||
.bind("created_when", atStateData.getCreation()).bind("state_data", atStateData.getStateData())
|
||||
.bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees())
|
||||
.bind("is_initial", atStateData.isInitial());
|
||||
return resultSet.getInt(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT state trim height from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
saveHelper.execute(this.repository);
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
this.repository.saveChanges();
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set AT state trim height in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareForAtStateTrimming() throws DataException {
|
||||
// Rebuild cache of latest AT states that we can't trim
|
||||
String deleteSql = "DELETE FROM LatestATStates";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(deleteSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to delete temporary latest AT states cache from repository", e);
|
||||
}
|
||||
|
||||
String insertSql = "INSERT INTO LatestATStates ("
|
||||
+ "SELECT AT_address, height FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||
+ ") "
|
||||
+ ")";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(insertSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimAtStates(int minHeight, int maxHeight, int limit) throws DataException {
|
||||
if (minHeight >= maxHeight)
|
||||
return 0;
|
||||
|
||||
// We're often called so no need to trim all states in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "DELETE FROM ATStatesData "
|
||||
+ "WHERE height BETWEEN ? AND ? "
|
||||
+ "AND NOT EXISTS("
|
||||
+ "SELECT TRUE FROM LatestATStates "
|
||||
+ "WHERE LatestATStates.AT_address = ATStatesData.AT_address "
|
||||
+ "AND LatestATStates.height = ATStatesData.height"
|
||||
+ ") "
|
||||
+ "LIMIT ?";
|
||||
|
||||
try {
|
||||
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight, limit);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to trim AT states in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ATStateData atStateData) throws DataException {
|
||||
// We shouldn't ever save partial ATStateData
|
||||
if (atStateData.getStateHash() == null || atStateData.getHeight() == null)
|
||||
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
|
||||
|
||||
HSQLDBSaver atStatesSaver = new HSQLDBSaver("ATStates");
|
||||
|
||||
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||
.bind("state_hash", atStateData.getStateHash())
|
||||
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
|
||||
|
||||
try {
|
||||
atStatesSaver.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
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
|
||||
public void delete(String atAddress, int height) throws DataException {
|
||||
try {
|
||||
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
|
||||
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?", atAddress, height);
|
||||
} catch (SQLException 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 {
|
||||
try {
|
||||
this.repository.delete("ATStates", "height = ?", height);
|
||||
this.repository.delete("ATStatesData", "height = ?", height);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete AT states from repository", e);
|
||||
}
|
||||
|
@ -462,13 +462,46 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
|
||||
// We're often called so no need to trim all blocks in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL LIMIT 1440";
|
||||
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 {
|
||||
return this.repository.executeCheckedUpdate(sql, timestamp);
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
this.repository.saveChanges();
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int trimOldOnlineAccountsSignatures(int minHeight, int maxHeight) throws DataException {
|
||||
// We're often called so no need to trim all blocks in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "UPDATE Blocks SET online_accounts_signatures = NULL "
|
||||
+ "WHERE online_accounts_signatures IS NOT NULL "
|
||||
+ "AND height BETWEEN ? AND ?";
|
||||
|
||||
try {
|
||||
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to trim old online accounts signatures in repository", e);
|
||||
|
@ -21,11 +21,16 @@ public class HSQLDBDatabaseUpdates {
|
||||
/**
|
||||
* Apply any incremental changes to database schema.
|
||||
*
|
||||
* @return true if database was non-existent/empty, false otherwise
|
||||
* @throws SQLException
|
||||
*/
|
||||
public static void updateDatabase(Connection connection) throws SQLException {
|
||||
while (databaseUpdating(connection))
|
||||
public static boolean updateDatabase(Connection connection) throws SQLException {
|
||||
final boolean wasPristine = fetchDatabaseVersion(connection) == 0;
|
||||
|
||||
while (databaseUpdating(connection, wasPristine))
|
||||
incrementDatabaseVersion(connection);
|
||||
|
||||
return wasPristine;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,23 +48,21 @@ public class HSQLDBDatabaseUpdates {
|
||||
/**
|
||||
* Fetch current version of database schema.
|
||||
*
|
||||
* @return int, 0 if no schema yet
|
||||
* @return database version, or 0 if no schema yet
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static int fetchDatabaseVersion(Connection connection) throws SQLException {
|
||||
int databaseVersion = 0;
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
if (stmt.execute("SELECT version FROM DatabaseInfo"))
|
||||
try (ResultSet resultSet = stmt.getResultSet()) {
|
||||
if (resultSet.next())
|
||||
databaseVersion = resultSet.getInt(1);
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
// empty database
|
||||
}
|
||||
|
||||
return databaseVersion;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,7 +71,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
* @return true - if a schema update happened, false otherwise
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static boolean databaseUpdating(Connection connection) throws SQLException {
|
||||
private static boolean databaseUpdating(Connection connection, boolean wasPristine) throws SQLException {
|
||||
int databaseVersion = fetchDatabaseVersion(connection);
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
@ -215,6 +218,8 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "PRIMARY KEY (account))");
|
||||
// For looking up an account by 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
|
||||
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)");
|
||||
// Add CHECK constraint to account balances
|
||||
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
|
||||
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)");
|
||||
// For finding per-block AT states, ordered by creation timestamp
|
||||
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
|
||||
stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, "
|
||||
@ -658,6 +667,115 @@ public class HSQLDBDatabaseUpdates {
|
||||
break;
|
||||
|
||||
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
|
||||
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");
|
||||
|
@ -24,6 +24,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -53,12 +54,27 @@ public class HSQLDBRepository implements Repository {
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
|
||||
|
||||
protected Connection connection;
|
||||
protected Deque<Savepoint> savepoints = new ArrayDeque<>(3);
|
||||
protected final Deque<Savepoint> savepoints = new ArrayDeque<>(3);
|
||||
protected boolean debugState = false;
|
||||
protected Long slowQueryThreshold = null;
|
||||
protected List<String> sqlStatements;
|
||||
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
|
||||
|
||||
@ -92,67 +108,67 @@ public class HSQLDBRepository implements Repository {
|
||||
|
||||
@Override
|
||||
public ATRepository getATRepository() {
|
||||
return new HSQLDBATRepository(this);
|
||||
return this.atRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountRepository getAccountRepository() {
|
||||
return new HSQLDBAccountRepository(this);
|
||||
return this.accountRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArbitraryRepository getArbitraryRepository() {
|
||||
return new HSQLDBArbitraryRepository(this);
|
||||
return this.arbitraryRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetRepository getAssetRepository() {
|
||||
return new HSQLDBAssetRepository(this);
|
||||
return this.assetRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockRepository getBlockRepository() {
|
||||
return new HSQLDBBlockRepository(this);
|
||||
return this.blockRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatRepository getChatRepository() {
|
||||
return new HSQLDBChatRepository(this);
|
||||
return this.chatRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossChainRepository getCrossChainRepository() {
|
||||
return new HSQLDBCrossChainRepository(this);
|
||||
return this.crossChainRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupRepository getGroupRepository() {
|
||||
return new HSQLDBGroupRepository(this);
|
||||
return this.groupRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRepository getMessageRepository() {
|
||||
return new HSQLDBMessageRepository(this);
|
||||
return this.messageRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NameRepository getNameRepository() {
|
||||
return new HSQLDBNameRepository(this);
|
||||
return this.nameRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkRepository getNetworkRepository() {
|
||||
return new HSQLDBNetworkRepository(this);
|
||||
return this.networkRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionRepository getTransactionRepository() {
|
||||
return new HSQLDBTransactionRepository(this);
|
||||
return this.transactionRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VotingRepository getVotingRepository() {
|
||||
return new HSQLDBVotingRepository(this);
|
||||
return this.votingRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -169,8 +185,20 @@ public class HSQLDBRepository implements Repository {
|
||||
|
||||
@Override
|
||||
public void saveChanges() throws DataException {
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
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) {
|
||||
throw new DataException("commit error", e);
|
||||
} finally {
|
||||
@ -194,7 +222,7 @@ public class HSQLDBRepository implements Repository {
|
||||
this.savepoints.clear();
|
||||
|
||||
// Before clearing statements so we can log what led to assertion error
|
||||
assertEmptyTransaction("transaction commit");
|
||||
assertEmptyTransaction("transaction rollback");
|
||||
|
||||
if (this.sqlStatements != null)
|
||||
this.sqlStatements.clear();
|
||||
@ -284,12 +312,13 @@ public class HSQLDBRepository implements Repository {
|
||||
Path oldRepoDirPath = Paths.get(dbPathname).getParent();
|
||||
|
||||
// Delete old repository files
|
||||
Files.walk(oldRepoDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
// Nothing to remove
|
||||
} catch (SQLException | IOException e) {
|
||||
@ -328,11 +357,12 @@ public class HSQLDBRepository implements Repository {
|
||||
Path backupDirPath = Paths.get(backupPathname).getParent();
|
||||
String backupDirPathname = backupDirPath.toString();
|
||||
|
||||
Files.walk(backupDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(backupDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(backupDirPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
// Nothing to remove
|
||||
} 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
|
||||
public void performPeriodicMaintenance() throws DataException {
|
||||
// Defrag DB - takes a while!
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
LOGGER.info("performing maintenance - this will take a while");
|
||||
stmt.execute("CHECKPOINT");
|
||||
stmt.execute("CHECKPOINT DEFRAG");
|
||||
LOGGER.info("maintenance completed");
|
||||
} catch (SQLException e) {
|
||||
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". */
|
||||
private static String getDbPathname(String connectionUrl) {
|
||||
/*package*/ static String getDbPathname(String connectionUrl) {
|
||||
Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)");
|
||||
Matcher matcher = pattern.matcher(connectionUrl);
|
||||
|
||||
@ -394,11 +462,12 @@ public class HSQLDBRepository implements Repository {
|
||||
LOGGER.info("Attempting repository recovery using backup");
|
||||
|
||||
// Move old repository files out the way
|
||||
Files.walk(oldRepoDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// Now "backup" the backup back to original repository location (the parent).
|
||||
@ -438,6 +507,10 @@ public class HSQLDBRepository implements Repository {
|
||||
if (this.sqlStatements != null)
|
||||
this.sqlStatements.add(sql);
|
||||
|
||||
return cachePreparedStatement(sql);
|
||||
}
|
||||
|
||||
private PreparedStatement cachePreparedStatement(String sql) throws SQLException {
|
||||
/*
|
||||
* We cache a duplicate PreparedStatement for this SQL string,
|
||||
* 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.
|
||||
*/
|
||||
if (!this.preparedStatementCache.containsKey(sql))
|
||||
this.preparedStatementCache.put(sql, this.connection.prepareStatement(sql));
|
||||
PreparedStatement preparedStatement = this.preparedStatementCache.get(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 {
|
||||
PreparedStatement preparedStatement = this.prepareStatement(sql);
|
||||
|
||||
// Close the PreparedStatement when the ResultSet is closed otherwise there's a potential resource leak.
|
||||
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
|
||||
preparedStatement.closeOnCompletion();
|
||||
// We don't close the PreparedStatement when the ResultSet is closed because we cached PreparedStatements now.
|
||||
// They are cleaned up when connection/session is closed.
|
||||
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
|
||||
@ -477,7 +560,7 @@ public class HSQLDBRepository implements Repository {
|
||||
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"));
|
||||
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
@ -556,7 +639,7 @@ public class HSQLDBRepository implements Repository {
|
||||
if (batchedObjects == null || batchedObjects.isEmpty())
|
||||
return 0;
|
||||
|
||||
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
|
||||
PreparedStatement preparedStatement = this.prepareStatement(sql);
|
||||
for (Object[] objects : batchedObjects) {
|
||||
this.bindStatementParams(preparedStatement, objects);
|
||||
preparedStatement.addBatch();
|
||||
@ -570,7 +653,7 @@ public class HSQLDBRepository implements Repository {
|
||||
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"));
|
||||
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
@ -586,7 +669,6 @@ public class HSQLDBRepository implements Repository {
|
||||
|
||||
return totalCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch last value of IDENTITY column after an INSERT statement.
|
||||
@ -765,15 +847,15 @@ public class HSQLDBRepository implements Repository {
|
||||
if (this.sqlStatements == null)
|
||||
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)
|
||||
LOGGER.info(sql);
|
||||
LOGGER.info(() -> String.format("[Session %d] %s", this.sessionId, sql));
|
||||
}
|
||||
|
||||
/** Logs other HSQLDB sessions then returns passed exception */
|
||||
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();
|
||||
|
||||
@ -807,14 +889,19 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
try (ResultSet resultSet = stmt.getResultSet()) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -822,7 +909,11 @@ public class HSQLDBRepository implements Repository {
|
||||
int transactionCount = resultSet.getInt(2);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
||||
|
||||
private String connectionUrl;
|
||||
private HSQLDBPool connectionPool;
|
||||
private final boolean wasPristine;
|
||||
|
||||
/**
|
||||
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt>.
|
||||
@ -65,12 +66,17 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
||||
|
||||
// Perform DB updates?
|
||||
try (final Connection connection = this.connectionPool.getConnection()) {
|
||||
HSQLDBDatabaseUpdates.updateDatabase(connection);
|
||||
this.wasPristine = HSQLDBDatabaseUpdates.updateDatabase(connection);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Repository initialization error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasPristineAtOpen() {
|
||||
return this.wasPristine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryFactory reopen() throws DataException {
|
||||
return new HSQLDBRepositoryFactory(this.connectionUrl);
|
||||
|
@ -60,7 +60,9 @@ public class HSQLDBSaver {
|
||||
*/
|
||||
public boolean execute(HSQLDBRepository repository) throws SQLException {
|
||||
String sql = this.formatInsertWithPlaceholders();
|
||||
try (PreparedStatement preparedStatement = repository.prepareStatement(sql)) {
|
||||
|
||||
try {
|
||||
PreparedStatement preparedStatement = repository.prepareStatement(sql);
|
||||
this.bindValues(preparedStatement);
|
||||
|
||||
return preparedStatement.execute();
|
||||
|
@ -5,6 +5,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
@ -42,6 +43,9 @@ public class Settings {
|
||||
// Settings, and other config files
|
||||
private String userPath;
|
||||
|
||||
// General
|
||||
private String localeLang = Locale.getDefault().getLanguage();
|
||||
|
||||
// Common to all networking (API/P2P)
|
||||
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
|
||||
|
||||
@ -62,6 +66,7 @@ public class Settings {
|
||||
"::1", "127.0.0.1"
|
||||
};
|
||||
private Boolean apiRestricted;
|
||||
private String apiKey = null;
|
||||
private boolean apiLoggingEnabled = false;
|
||||
private boolean apiDocumentationEnabled = false;
|
||||
// Both of these need to be set for API to use SSL
|
||||
@ -80,6 +85,26 @@ public class Settings {
|
||||
private long repositoryBackupInterval = 0; // ms
|
||||
/** Whether to show a notification when we backup repository. */
|
||||
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
|
||||
private boolean isTestNet = false;
|
||||
@ -253,6 +278,9 @@ public class Settings {
|
||||
// Validation goes here
|
||||
if (this.minBlockchainPeers < 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
|
||||
@ -261,6 +289,10 @@ public class Settings {
|
||||
return this.userPath;
|
||||
}
|
||||
|
||||
public String getLocaleLang() {
|
||||
return this.localeLang;
|
||||
}
|
||||
|
||||
public int getUiServerPort() {
|
||||
return this.uiPort;
|
||||
}
|
||||
@ -297,6 +329,10 @@ public class Settings {
|
||||
return !BlockChain.getInstance().isTestChain();
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
public boolean isApiLoggingEnabled() {
|
||||
return this.apiLoggingEnabled;
|
||||
}
|
||||
@ -412,4 +448,36 @@ public class Settings {
|
||||
return this.showBackupNotification;
|
||||
}
|
||||
|
||||
public long getRepositoryCheckpointInterval() {
|
||||
return this.repositoryCheckpointInterval;
|
||||
}
|
||||
|
||||
public boolean getShowCheckpointNotification() {
|
||||
return this.showCheckpointNotification;
|
||||
}
|
||||
|
||||
public long getAtStatesMaxLifetime() {
|
||||
return this.atStatesMaxLifetime;
|
||||
}
|
||||
|
||||
public long getAtStatesTrimInterval() {
|
||||
return this.atStatesTrimInterval;
|
||||
}
|
||||
|
||||
public int getAtStatesTrimBatchSize() {
|
||||
return this.atStatesTrimBatchSize;
|
||||
}
|
||||
|
||||
public int getAtStatesTrimLimit() {
|
||||
return this.atStatesTrimLimit;
|
||||
}
|
||||
|
||||
public long getOnlineSignaturesTrimInterval() {
|
||||
return this.onlineSignaturesTrimInterval;
|
||||
}
|
||||
|
||||
public int getOnlineSignaturesTrimBatchSize() {
|
||||
return this.onlineSignaturesTrimBatchSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
|
||||
INVALID_ADDRESS = ung\u00FCltige adresse
|
||||
INVALID_ADDRESS = ungültige adresse
|
||||
|
||||
INVALID_ASSET_ID = ung\u00FCltige asset ID
|
||||
INVALID_ASSET_ID = ungültige asset ID
|
||||
|
||||
INVALID_DATA = ung\u00FCltige daten
|
||||
INVALID_DATA = ungültige daten
|
||||
|
||||
INVALID_PUBLIC_KEY = ung\u00FCltiger public key
|
||||
INVALID_PUBLIC_KEY = ungültiger public key
|
||||
|
||||
INVALID_SIGNATURE = ung\u00FCltige signatur
|
||||
INVALID_SIGNATURE = ungültige signatur
|
||||
|
||||
JSON = JSON nachricht konnte nicht geparsed werden
|
||||
|
||||
|
@ -1,53 +1,57 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# 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
|
||||
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
|
||||
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
|
||||
INVALID_SIGNATURE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
|
||||
INVALID_SIGNATURE = недействительная подпись
|
||||
|
||||
JSON = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D0\u00B1\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 json
|
||||
JSON = не удалось разобрать сообщение json
|
||||
|
||||
NAME_UNKNOWN = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00BE
|
||||
NAME_UNKNOWN = имя неизвестно
|
||||
|
||||
ORDER_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
NON_PRODUCTION = этот вызов API не разрешен для производственных систем
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B9\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD
|
||||
ORDER_UNKNOWN = неизвестный идентификатор заказа актива
|
||||
|
||||
REPOSITORY_ISSUE = \u00D0\u00BE\u00D1\u0088\u00D0\u00B8\u00D0\u00B1\u00D0\u00BA\u00D0\u00B0 \u00D1\u0080\u00D0\u00B5\u00D0\u00BF\u00D0\u00BE\u00D0\u00B7\u00D0\u00B8\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D1\u008F
|
||||
PUBLIC_KEY_NOT_FOUND = открытый ключ не найден
|
||||
|
||||
TRANSACTION_INVALID = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0: %s (%s)
|
||||
REPOSITORY_ISSUE = ошибка репозитория
|
||||
|
||||
TRANSACTION_UNKNOWN = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0
|
||||
TRANSACTION_INVALID = транзакция недействительна: %s (%s)
|
||||
|
||||
TRANSFORMATION_ERROR = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C JSON \u00D0\u00B2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008E
|
||||
TRANSACTION_UNKNOWN = транзакция неизвестна
|
||||
|
||||
UNAUTHORIZED = \u00D0\u00B2\u00D1\u008B\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2 API \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B2\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD
|
||||
TRANSFORMATION_ERROR = не удалось преобразовать JSON в транзакцию
|
||||
|
||||
UNAUTHORIZED = вызов API не авторизован
|
||||
|
@ -19,6 +19,8 @@ CREATING_BACKUP_OF_DB_FILES = Creating backup of database files...
|
||||
|
||||
DB_BACKUP = Database Backup
|
||||
|
||||
DB_CHECKPOINT = Database Checkpoint
|
||||
|
||||
EXIT = Exit
|
||||
|
||||
MINTING_DISABLED = NOT minting
|
||||
@ -34,6 +36,8 @@ NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
|
||||
|
||||
OPEN_UI = Open UI
|
||||
|
||||
PERFORMING_DB_CHECKPOINT = Saving uncommitted database changes...
|
||||
|
||||
SYNCHRONIZE_CLOCK = Synchronize clock
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
|
||||
|
@ -1,31 +1,31 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# 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
|
||||
NTP_NAG_CAPTION = \u7535\u8111\u7684\u65F6\u949F\u4E0D\u51C6\u786E\uFF01
|
||||
NTP_NAG_CAPTION = 电脑的时钟不准确!
|
||||
|
||||
NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7684\u65F6\u949F\u3002
|
||||
NTP_NAG_TEXT_UNIX = 安装NTP服务以获得准确的时钟。
|
||||
|
||||
NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002
|
||||
NTP_NAG_TEXT_WINDOWS = 从菜单中选择“同步时钟”进行修复。
|
||||
|
||||
OPEN_UI = \u5F00\u542F\u754C\u9762
|
||||
OPEN_UI = 开启界面
|
||||
|
||||
SYNCHRONIZE_CLOCK = \u540C\u6B65\u65F6\u949F
|
||||
SYNCHRONIZE_CLOCK = 同步时钟
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = \u540C\u6B65\u533A\u5757\u94FE
|
||||
SYNCHRONIZING_BLOCKCHAIN = 同步区块链
|
||||
|
||||
SYNCHRONIZING_CLOCK = \u540C\u6B65\u7740\u65F6\u949F
|
||||
SYNCHRONIZING_CLOCK = 同步着时钟
|
||||
|
@ -1,164 +1,176 @@
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
ACCOUNT_ALREADY_EXISTS = аккаунт уже существует
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5\u00D0\u00BC
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = аккаунт не может делиться вознаграждением
|
||||
|
||||
ALREADY_GROUP_ADMIN = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
ALREADY_GROUP_ADMIN = уже администратор группы
|
||||
|
||||
ALREADY_GROUP_MEMBER = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
ALREADY_GROUP_MEMBER = уже член группы
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B3\u00D0\u00BE\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BB\u00D0\u00B8 \u00D0\u00B7\u00D0\u00B0 \u00D1\u008D\u00D1\u0082\u00D0\u00BE\u00D1\u0082 \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = уже проголосовали за этот вариант
|
||||
|
||||
ASSET_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
ASSET_ALREADY_EXISTS = актив уже существует
|
||||
|
||||
ASSET_DOES_NOT_EXIST = \u00D0\u0090\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
ASSET_DOES_NOT_EXIST = Актив не существует
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00BF\u00D0\u00B0\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082 \u00D1\u0081 \u00D0\u0090\u00D0\u00A2
|
||||
ASSET_DOES_NOT_MATCH_AT = актив не совпадает с АТ
|
||||
|
||||
AT_ALREADY_EXISTS = AT \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
ASSET_NOT_SPENDABLE = актив не подлежит расходованию
|
||||
|
||||
AT_IS_FINISHED = AT \u00D0\u00B2 \u00D0\u00B7\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B8
|
||||
AT_ALREADY_EXISTS = AT уже существует
|
||||
|
||||
AT_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u0090\u00D0\u00A2
|
||||
AT_IS_FINISHED = AT в завершении
|
||||
|
||||
BANNED_FROM_GROUP = \u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087\u00D0\u00B5\u00D0\u00BD \u00D0\u00B8\u00D0\u00B7 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
AT_UNKNOWN = не известный АТ
|
||||
|
||||
BAN_EXISTS = \u00D0\u0091\u00D0\u00B0\u00D0\u00BD
|
||||
BANNED_FROM_GROUP = исключен из группы
|
||||
|
||||
BAN_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BD
|
||||
BAN_EXISTS = Бан
|
||||
|
||||
BUYER_ALREADY_OWNER = \u00D0\u00BF\u00D0\u00BE\u00D0\u00BA\u00D1\u0083\u00D0\u00BF\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B1\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D0\u00BD\u00D0\u00BD\u00D0\u00B8\u00D0\u00BA
|
||||
BAN_UNKNOWN = не известный бан
|
||||
|
||||
DUPLICATE_OPTION = \u00D0\u00B4\u00D1\u0083\u00D0\u00B1\u00D0\u00BB\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
||||
BUYER_ALREADY_OWNER = покупатель уже собственник
|
||||
|
||||
GROUP_ALREADY_EXISTS = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
CLOCK_NOT_SYNCED = часы не синхронизированы
|
||||
|
||||
GROUP_APPROVAL_DECIDED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
||||
DUPLICATE_OPTION = дублировать вариант
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00BE\u00D0\u00B2\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0082\u00D1\u0080\u00D0\u00B5\u00D0\u00B1\u00D1\u0083\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
||||
GROUP_ALREADY_EXISTS = группа уже существует
|
||||
|
||||
GROUP_DOES_NOT_EXIST = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
GROUP_APPROVAL_DECIDED = гуппа одобрена
|
||||
|
||||
GROUP_ID_MISMATCH = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
GROUP_APPROVAL_NOT_REQUIRED = гупповое одобрение не требуется
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0083\u00D0\u00B9\u00D1\u0082\u00D0\u00B8
|
||||
GROUP_DOES_NOT_EXIST = группа не существует
|
||||
|
||||
HAVE_EQUALS_WANT = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D0\u00B5\u00D1\u008E\u00D1\u0082\u00D1\u0081\u00D1\u008F \u00D1\u0080\u00D0\u00B0\u00D0\u00B2\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B6\u00D0\u00B5\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
GROUP_ID_MISMATCH = не соответствие идентификатора группы
|
||||
|
||||
INSUFFICIENT_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B0
|
||||
GROUP_OWNER_CANNOT_LEAVE = владелец группы не может уйти
|
||||
|
||||
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
||||
HAVE_EQUALS_WANT = иммеются равные желания
|
||||
|
||||
INVALID_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
||||
INSUFFICIENT_FEE = недостаточная плата
|
||||
|
||||
INVALID_ASSET_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
INVALID_ADDRESS = недействительный адрес
|
||||
|
||||
INVALID_AT_TRANSACTION = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u0090\u00D0\u00A2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
||||
INVALID_AMOUNT = недопустимая сумма
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE \u00D0\u00B4\u00D0\u00BB\u00D1\u008F \u00D1\u0082\u00D0\u00B8\u00D0\u00BF\u00D0\u00B0 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B AT
|
||||
INVALID_ASSET_OWNER = недействительный владелец актива
|
||||
|
||||
INVALID_CREATION_BYTES = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B5 \u00D0\u00B1\u00D0\u00B0\u00D0\u00B9\u00D1\u0082\u00D1\u008B \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
INVALID_AT_TRANSACTION = недействительная АТ транзакция
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
INVALID_AT_TYPE_LENGTH = недействительно для типа длины AT
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D1\u0080\u00D0\u00BE\u00D0\u00B3 \u00D1\u0083\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
INVALID_CREATION_BYTES = недопустимые байты создания
|
||||
|
||||
INVALID_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
INVALID_DATA_LENGTH = недопустимая длина данных
|
||||
|
||||
INVALID_GROUP_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
INVALID_DESCRIPTION_LENGTH = недопустимая длина описания
|
||||
|
||||
INVALID_LIFETIME = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D1\u0081\u00D1\u0080\u00D0\u00BE\u00D0\u00BA \u00D1\u0081\u00D0\u00BB\u00D1\u0083\u00D0\u00B6\u00D0\u00B1\u00D1\u008B
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = недопустимый порог утверждения группы
|
||||
|
||||
INVALID_NAME_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
INVALID_GROUP_ID = недопустимый идентификатор группы
|
||||
|
||||
INVALID_NAME_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D1\u0086\u00D0\u00B0
|
||||
INVALID_GROUP_OWNER = недопу владелец группы
|
||||
|
||||
INVALID_OPTIONS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B9
|
||||
INVALID_LIFETIME = недопу срок службы
|
||||
|
||||
INVALID_OPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B8
|
||||
INVALID_NAME_LENGTH = недопустимая длина группы
|
||||
|
||||
INVALID_ORDER_CREATOR = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0
|
||||
INVALID_NAME_OWNER = недопустимое имя владельца
|
||||
|
||||
INVALID_PAYMENTS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00B6\u00D0\u00B5\u00D0\u00B9
|
||||
INVALID_OPTIONS_COUNT = неверное количество опций
|
||||
|
||||
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
||||
INVALID_OPTION_LENGTH = недопустимая длина опции
|
||||
|
||||
INVALID_QUANTITY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE
|
||||
INVALID_ORDER_CREATOR = недопустимый создатель заказа
|
||||
|
||||
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
|
||||
INVALID_PAYMENTS_COUNT = неверный подсчет платежей
|
||||
|
||||
INVALID_RETURN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00B2\u00D1\u0080\u00D0\u00B0\u00D1\u0082
|
||||
INVALID_PUBLIC_KEY = недействительный открытый ключ
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0086\u00D0\u00B5\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
INVALID_QUANTITY = недопустимое количество
|
||||
|
||||
INVALID_SELLER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0086
|
||||
INVALID_REFERENCE = неверная ссылка
|
||||
|
||||
INVALID_TAGS_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D1\u0082\u00D1\u008D\u00D0\u00B3\u00D0\u00BE\u00D0\u00B2
|
||||
INVALID_RETURN = недопустимый возврат
|
||||
|
||||
INVALID_TX_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BF\u00D0\u00B5\u00D1\u0080\u00D0\u00B5\u00D0\u00B4\u00D0\u00B0\u00D1\u0087\u00D0\u00B8
|
||||
INVALID_REWARD_SHARE_PERCENT = недействительный процент награждения
|
||||
|
||||
INVALID_VALUE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D1\u0087\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B
|
||||
INVALID_SELLER = недействительный продавец
|
||||
|
||||
JOIN_REQUEST_EXISTS = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D0\u00BD\u00D0\u00B0 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D1\u0081\u00D0\u00BE\u00D0\u00B5\u00D0\u00B4\u00D0\u00B8\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
INVALID_TAGS_LENGTH = недействительная длина тэгов
|
||||
|
||||
MAXIMUM_REWARD_SHARES = \u00D0\u00BC\u00D0\u00B0\u00D0\u00BA\u00D1\u0081\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
|
||||
INVALID_TX_GROUP_ID = недействительный идентификатор группы передачи
|
||||
|
||||
MISSING_CREATOR = \u00D0\u00BE\u00D1\u0082\u00D1\u0081\u00D1\u0083\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D1\u008E\u00D1\u0089\u00D0\u00B8\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C
|
||||
INVALID_VALUE_LENGTH = недопустимое значение длины
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = \u00D0\u00BD\u00D0\u00B5\u00D1\u0081\u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D1\u008C\u00D0\u00BA\u00D0\u00BE \u00D0\u00B8\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
||||
INVITE_UNKNOWN = приглашать неизветсных
|
||||
|
||||
NAME_ALREADY_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B2 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B6\u00D0\u00B5
|
||||
JOIN_REQUEST_EXISTS = запрос на присоединение существует
|
||||
|
||||
NAME_ALREADY_REGISTERED = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B7\u00D0\u00B0\u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD\u00D0\u00BE
|
||||
MAXIMUM_REWARD_SHARES = максимальное вознаграждение
|
||||
|
||||
NAME_DOES_NOT_EXIST = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
MISSING_CREATOR = отсутствующий создатель
|
||||
|
||||
NAME_NOT_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
||||
MULTIPLE_NAMES_FORBIDDEN = несколько имен запрещено
|
||||
|
||||
NAME_NOT_LOWER_CASE = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B6\u00D0\u00BD\u00D0\u00BE \u00D1\u0081\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080
|
||||
NAME_ALREADY_FOR_SALE = имя уже в продаже
|
||||
|
||||
NEGATIVE_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
||||
NAME_ALREADY_REGISTERED = имя уже зарегистрировано
|
||||
|
||||
NEGATIVE_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BA\u00D0\u00BE\u00D0\u00BC\u00D0\u00B8\u00D1\u0081\u00D1\u0081\u00D0\u00B8\u00D1\u008F
|
||||
NAME_DOES_NOT_EXIST = имя не существует
|
||||
|
||||
NEGATIVE_PRICE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D1\u008C
|
||||
NAME_NOT_FOR_SALE = имя не продается
|
||||
|
||||
NOT_GROUP_ADMIN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
NAME_NOT_LOWER_CASE = иммя не должно содержать строчный регистр
|
||||
|
||||
NOT_GROUP_MEMBER = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
NEGATIVE_AMOUNT = недостаточная сумма
|
||||
|
||||
NOT_MINTING_ACCOUNT = \u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082
|
||||
NEGATIVE_FEE = недостаточная комиссия
|
||||
|
||||
NOT_YET_RELEASED = \u00D0\u00B5\u00D1\u0089\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B2\u00D1\u008B\u00D0\u00BF\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
||||
NEGATIVE_PRICE = недостаточная стоимость
|
||||
|
||||
NO_BALANCE = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D1\u0081\u00D0\u00B0
|
||||
NOT_GROUP_ADMIN = не администратор группы
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D1\u0087\u00D0\u00B5\u00D0\u00B9\u00D0\u00BD \u00D1\u0083\u00D0\u00B7\u00D0\u00BB\u00D0\u00B0 \u00D0\u00B2 \u00D0\u00BD\u00D0\u00B0\u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D1\u008F\u00D1\u0089\u00D0\u00B5\u00D0\u00B5 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BD\u00D1\u008F\u00D1\u0082
|
||||
NOT_GROUP_MEMBER = не член группы
|
||||
|
||||
NO_FLAG_PERMISSION = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D1\u0080\u00D0\u00B5\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B0 \u00D1\u0084\u00D0\u00BB\u00D0\u00B0\u00D0\u00B3
|
||||
NOT_MINTING_ACCOUNT = счет не чеканит
|
||||
|
||||
NOT_YET_RELEASED = еще не выпущено
|
||||
|
||||
NO_BALANCE = нет баланса
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = блокчейн узла в настоящее время занят
|
||||
|
||||
NO_FLAG_PERMISSION = нет разрешения на флаг
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082
|
||||
ORDER_ALREADY_CLOSED = заказ закрыт
|
||||
|
||||
ORDER_DOES_NOT_EXIST = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
ORDER_DOES_NOT_EXIST = заказа не существует
|
||||
|
||||
POLL_ALREADY_EXISTS = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
POLL_ALREADY_EXISTS = опрос уже существует
|
||||
|
||||
POLL_DOES_NOT_EXIST = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
POLL_DOES_NOT_EXIST = опроса не существует
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082\u00D0\u00BE\u00D0\u00B2 \u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
POLL_OPTION_DOES_NOT_EXIST = вариантов ответа не существует
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B5\u00D0\u00BD
|
||||
PUBLIC_KEY_UNKNOWN = открытый ключ неизвестен
|
||||
|
||||
SELF_SHARE_EXISTS = \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B5\u00D0\u00B9
|
||||
SELF_SHARE_EXISTS = поделиться долей
|
||||
|
||||
TIMESTAMP_TOO_NEW = \u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
||||
TIMESTAMP_TOO_NEW = новая метка времени
|
||||
|
||||
TIMESTAMP_TOO_OLD = \u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0080\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
||||
TIMESTAMP_TOO_OLD = старая метка времени
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
||||
TOO_MANY_UNCONFIRMED = много не подтвержденных
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
TRANSACTION_ALREADY_CONFIRMED = транзакция уже подтверждена
|
||||
|
||||
TRANSACTION_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
||||
TRANSACTION_ALREADY_EXISTS = транзакция существует
|
||||
|
||||
TRANSACTION_UNKNOWN = неизвестная транзакция
|
||||
|
||||
TX_GROUP_ID_MISMATCH = не соответствие идентификатора группы c хэш транзации
|
||||
|
@ -1,6 +1,10 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Before;
|
||||
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
|
||||
public void testCommonBlockSearch() {
|
||||
// Given a list of block summaries, trim all trailing summaries after common block
|
||||
|
@ -103,8 +103,10 @@ public class ChainWeightTests extends Common {
|
||||
populateBlockSummariesMinterLevels(repository, shorterChain);
|
||||
populateBlockSummariesMinterLevels(repository, longerChain);
|
||||
|
||||
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain);
|
||||
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain);
|
||||
final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size());
|
||||
|
||||
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));
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ public class RepositoryTests extends Common {
|
||||
BlockUtils.mintBlock(repository1);
|
||||
|
||||
// Perform database 'update', but don't commit at this stage
|
||||
repository1.getBlockRepository().trimOldOnlineAccountsSignatures(System.currentTimeMillis());
|
||||
repository1.getBlockRepository().trimOldOnlineAccountsSignatures(1, 10);
|
||||
|
||||
// Open connection 2
|
||||
try (final Repository repository2 = RepositoryManager.getRepository()) {
|
||||
|
426
src/test/java/org/qortal/test/at/AtRepositoryTests.java
Normal file
426
src/test/java/org/qortal/test/at/AtRepositoryTests.java
Normal file
@ -0,0 +1,426 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
|
||||
public class AtRepositoryTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetATStateAtHeightWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
Integer testHeight = 8;
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetATStateAtHeightWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
int maxHeight = 8;
|
||||
Integer testHeight = maxHeight - 2;
|
||||
|
||||
// Trim AT state data
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLatestATStateWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight;
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLatestATStatePostTrimming() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
int maxHeight = blockchainHeight + 100; // more than latest block height
|
||||
Integer testHeight = blockchainHeight;
|
||||
|
||||
// Trim AT state data
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
// We should always have the latest AT state data available
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight;
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
|
||||
byte[] codeHash = atData.getCodeHash();
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = null;
|
||||
Long expectedValue = null;
|
||||
Integer minimumFinalHeight = null;
|
||||
Integer limit = null;
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||
codeHash,
|
||||
isFinished,
|
||||
dataByteOffset,
|
||||
expectedValue,
|
||||
minimumFinalHeight,
|
||||
limit, offset, reverse);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMatchingFinalATStatesWithDataValue() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight;
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
|
||||
byte[] codeHash = atData.getCodeHash();
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = MachineState.HEADER_LENGTH + 0;
|
||||
Long expectedValue = 0L;
|
||||
Integer minimumFinalHeight = null;
|
||||
Integer limit = null;
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||
codeHash,
|
||||
isFinished,
|
||||
dataByteOffset,
|
||||
expectedValue,
|
||||
minimumFinalHeight,
|
||||
limit, offset, reverse);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBlockATStatesAtHeightWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
Integer testHeight = 8;
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
// getBlockATStatesAtHeight never returns actual AT state data anyway
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBlockATStatesAtHeightWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
int maxHeight = 8;
|
||||
Integer testHeight = maxHeight - 2;
|
||||
|
||||
// Trim AT state data
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
// getBlockATStatesAtHeight never returns actual AT state data anyway
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveATStateWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight - 2;
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
|
||||
repository.getATRepository().save(atStateData);
|
||||
|
||||
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveATStateWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight - 2;
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
|
||||
// Clear data
|
||||
ATStateData newAtStateData = new ATStateData(atStateData.getATAddress(),
|
||||
atStateData.getHeight(),
|
||||
/*StateData*/ null,
|
||||
atStateData.getStateHash(),
|
||||
atStateData.getFees(),
|
||||
atStateData.isInitial());
|
||||
repository.getATRepository().save(newAtStateData);
|
||||
|
||||
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildSimpleAT() {
|
||||
// Pretend we use 4 values in data segment
|
||||
int addrCounter = 4;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
}
|
@ -13,7 +13,7 @@ fi
|
||||
cd ${git_dir}
|
||||
|
||||
# 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/}
|
||||
echo "Current git branch: ${branch_name}"
|
||||
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 push --set-upstream origin --force-with-lease ${update_branch}
|
||||
|
||||
branch_name=${branch_name-master}
|
||||
|
||||
echo "Changing back to '${branch_name}' branch"
|
||||
git checkout --force ${branch_name}
|
||||
|
Loading…
Reference in New Issue
Block a user