Compare commits

...

45 Commits

Author SHA1 Message Date
catbref
e4e775a107 Bump to v1.3.8 2020-12-24 12:09:30 +00:00
catbref
5d6811bd50 Workaround for block 212937 issue 2020-12-23 15:02:14 +00:00
catbref
ecfa6e994e Add API call POST /admin/repository/backup to trigger immediate backup 2020-12-16 12:52:19 +00:00
catbref
ed4a45f214 Force blocking DB backup to improve integrity of backup files 2020-12-16 12:51:30 +00:00
catbref
e953be6e4a In pom.xml, have Maven surefire plugin skip tests by default 2020-12-14 15:25:53 +00:00
catbref
bd51806a0d Improve Block.getBytesForMinterSignature() 2020-12-14 15:05:31 +00:00
catbref
625dbfbbd7 Unify BlockInfo into BlockSummaryData, removing minterAddress (unused) and extra repository calls 2020-12-14 13:13:44 +00:00
catbref
fc7a7a1549 Bump Maven surefire plugin to v2.22.2 for better test/CI support 2020-11-24 15:11:39 +00:00
catbref
a12045c19e Fix repository race condition from using wrong synchronization object
Previous fixes for "transaction rollback: serialization failure" when updating trim heights
in commits 16397852 and 58ed7205 had the right idea but were broken due to being synchronized
on different objects.

this.repository.trimHeightsLock would be a new Object() for each repository connection/session
and so not actually synchronize concurrent updates.

Implicit saveChanges()/COMMIT is still needed.

Fix is to use a repository-wide object for synchronization - in this case the repositoryFactory
object as held by RepositoryManager.

Added test to cover.

Also reduced DB trim height read to one call at start of thread for both trimming threads.
2020-11-17 14:53:39 +00:00
catbref
62ae49b639 ApiError_de.properties & SysTray_zh.properties also converted to UTF8 2020-11-12 12:18:26 +00:00
catbref
2e8f58bb2f ApiError_ru.properties & TransactionValidity_ru.properties converted to UTF8 for easier management 2020-11-12 11:34:25 +00:00
catbref
9e98ce220f Revert SysTray_ru.properties back to UTF8 form 2020-11-12 11:28:01 +00:00
catbref
10c3a0c056 Update AdvancedInstaller config file with v1.3.7 values 2020-11-12 11:27:36 +00:00
catbref
69ec654e4a Bump to version 1.3.7 2020-11-11 09:27:25 +00:00
catbref
a310e751bb Fix slow SQL query in HSQLDBATRepository.getBlockATStatesAtHeight() - mostly used during orphaning 2020-11-10 16:58:24 +00:00
catbref
3ef8b81e51 Fix incorrect column indexes when fetching frozen AT data 2020-11-10 15:38:15 +00:00
catbref
1f409235e4 Don't rebuild repository or export node-local data during repository build if repository was 'pristine'.
Under certain conditions, e.g. non-existent database files, the repository would be created
and then immediately be re-created.

Not only was this unnecessary, but HSQLDBDatabaseUpdates would attempt to export the node-local
data twice, which would cause an error due to existing .script files.

The fix is three-pronged:

1. Don't immediately rebuild the repository if it's only just been built
2. Don't export the empty node-local data if repository has only just been built
3. Don't export the node-local data if it's empty
2020-11-09 10:31:21 +00:00
catbref
806baa6ae4 Fix API call referenced in DB reshape 2020-11-06 11:23:35 +00:00
catbref
58ed72058f Another attempt to prevent "serialization failure" during trimming 2020-11-05 14:36:14 +00:00
catbref
253a994438 Add API POST /repository/checkpoint call. Renamed GET/POST /admin/repository calls to /admin/repository/data 2020-11-05 11:08:54 +00:00
catbref
5549eded38 Improve/fix use of latest block cache, for more cache hits, faster chain-tip response, etc. 2020-11-05 09:34:57 +00:00
catbref
20777363cf Split AT state storage into two HSQLDB table for better management
This involves a database reshape, but before this happens the node-local
data is exported to local files, giving the user the option to use a
bootstrap file instead of waiting.
2020-11-04 20:07:30 +00:00
catbref
b3f859f290 Don't use WITH COLUMN NAMES when exporting data from repository into local file 2020-11-04 15:48:52 +00:00
catbref
8c9f68a9c3 Add API calls for exporting node-local repository data & corresponding import to/from local files 2020-11-04 15:35:42 +00:00
catbref
41f178bf59 Add support for API key security, where X-API-KEY header must match apiKey from settings
apiKey in settings is null by default at this point, for backwards compatibility.
In the future, the Windows installer could generate a UUID for apiKey.
apiKey in settings needs to be at least 8 characters.

API calls in the documentation engine are now marked with an open/closed padlock
to show where API key might be required.
Add support for API key security, where X-API-KEY header must match apiKey from settings

apiKey in settings is null by default at this point, for backwards compatibility.
In the future, the Windows installer could generate a UUID for apiKey.
apiKey in settings needs to be at least 8 characters.

API calls in the documentation engine are now marked with an open/closed padlock
to show where API key might be required.
2020-11-04 15:35:20 +00:00
catbref
ad5050f92e Add support for exporting node-local repository data to .script files and corresponding import function 2020-11-04 15:29:10 +00:00
catbref
16397852ae Add synchronization around updating trim heights to prevent deadlock/rollback 2020-11-04 10:01:20 +00:00
catbref
c125a53655 More (optionally) logging when comparing chains with peers. Support for potential future minor consensus change. 2020-11-02 11:52:06 +00:00
catbref
7b056a832f Turn off HSQLDB redo-log "blockchain.log" and periodically call "CHECKPOINT" instead.
Checkpointing interval is 1 hour by default, changable in settings via
"repositoryCheckpointInterval"
plus corresponding "showCheckpointNotifications" SysTray flags (off by default).

Added entries to SysTray_en i18n properties, and converted SysTray_ru to ISO-8559-1.
2020-11-02 11:49:21 +00:00
catbref
6c40727027 More reporting for slow HSQLDB queries/commits 2020-11-02 11:16:40 +00:00
catbref
8f06765caf Networking performance improvements and message sending bugfix 2020-11-02 11:15:53 +00:00
catbref
de2fc78ad1 Add support for SHA256 digest of ByteBuffer in Crypto class 2020-11-02 10:47:27 +00:00
catbref
ee08410260 More trace-level debugging in Synchronizer to help diagnose chain reorg issues 2020-11-02 10:46:51 +00:00
catbref
88da8d949f Don't allow latest blocks cache to be empty 2020-11-02 10:45:21 +00:00
catbref
d2a92db921 More caching for GetBlockMessage. Added API call GET /admin/enginestats to monitor cache usage 2020-10-29 11:02:02 +00:00
catbref
9c18a33d7f Improve tools/build-auto-update.sh when working on detached HEAD 2020-10-28 09:08:16 +00:00
catbref
f3b8258067 Add more latest block caching to reduce repository accesses, especially for requests from remote peers 2020-10-28 08:46:30 +00:00
catbref
da78c73485 Remove extraneous call to Controller.onNewBlock() after synchronization, as this call is performed per-block inside Synchronizer 2020-10-28 08:42:59 +00:00
catbref
cec25ce279 Add API call POST /peers/commonblock <connected-peer> as debugging aid 2020-10-28 08:41:23 +00:00
catbref
0389007491 Skip trimming while performing synchronization 2020-10-28 08:38:11 +00:00
catbref
38a64bdd9e Prevent HSQLDB prepared statement cache invalidation when rebuilding latest AT states cache 2020-10-12 14:35:10 +01:00
catbref
6a24f787c4 Bump to v1.3.6 2020-10-07 14:56:58 +01:00
catbref
98564aa8bf Fix SQL logic error when fetching trade offers 2020-10-07 14:56:05 +01:00
catbref
9ceff90f42 Upgrade to CIYAM-AT v1.3.8 with slight performance improvements 2020-10-07 10:31:18 +01:00
catbref
6a4388fecc Use cached PreparedStatement for HSQLDB.assertEmptyTransaction + other minor HSQLDB fixes 2020-10-07 09:45:44 +01:00
52 changed files with 3126 additions and 609 deletions

View File

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

Binary file not shown.

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>1.3.8</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -3,13 +3,14 @@
<groupId>org.ciyam</groupId>
<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
View File

@@ -3,13 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.3.5</version>
<version>1.3.8</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>
<bitcoinj.version>0.15.5</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>
@@ -312,6 +313,14 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>

View File

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

View File

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

View File

@@ -1,71 +0,0 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockInfo {
private byte[] signature;
private int height;
private long timestamp;
private int transactionCount;
private String minterAddress;
protected BlockInfo() {
/* For JAXB */
}
public BlockInfo(byte[] signature, int height, long timestamp, int transactionCount, String minterAddress) {
this.signature = signature;
this.height = height;
this.timestamp = timestamp;
this.transactionCount = transactionCount;
this.minterAddress = minterAddress;
}
public BlockInfo(BlockData blockData) {
// Convert BlockData to BlockInfo, using additional data
this.minterAddress = "unknown?";
try (final Repository repository = RepositoryManager.getRepository()) {
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(blockData.getMinterPublicKey());
if (rewardShareData != null)
this.minterAddress = rewardShareData.getMintingAccount();
} catch (DataException e) {
// We'll carry on with placeholder minterAddress then...
}
this.signature = blockData.getSignature();
this.height = blockData.getHeight();
this.timestamp = blockData.getTimestamp();
this.transactionCount = blockData.getTransactionCount();
}
public byte[] getSignature() {
return this.signature;
}
public int getHeight() {
return this.height;
}
public long getTimestamp() {
return this.timestamp;
}
public int getTransactionCount() {
return this.transactionCount;
}
public String getMinterAddress() {
return this.minterAddress;
}
}

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.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);

View File

@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.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;
@@ -133,6 +134,7 @@ public class AdminResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public NodeStatus status() {
Security.checkApiCallAllowed(request);
@@ -153,6 +155,7 @@ public class AdminResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public String shutdown() {
Security.checkApiCallAllowed(request);
@@ -181,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();
@@ -193,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) {
@@ -210,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(
@@ -222,6 +249,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<MintingAccountData> getMintingAccounts() {
Security.checkApiCallAllowed(request);
@@ -268,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);
@@ -320,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);
@@ -419,6 +449,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String orphan(String targetHeightString) {
Security.checkApiCallAllowed(request);
@@ -460,6 +491,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String forceSync(String targetPeerAddress) {
Security.checkApiCallAllowed(request);
@@ -498,6 +530,162 @@ 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);
}
}
@POST
@Path("/repository/backup")
@Operation(
summary = "Perform online backup of repository.",
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String backupRepository() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.backup(true);
repository.saveChanges();
return "true";
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform backup
return "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@DELETE
@Path("/repository")
@Operation(
@@ -505,6 +693,7 @@ public class AdminResource {
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);

View File

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

View File

@@ -23,7 +23,6 @@ import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
@@ -492,7 +491,7 @@ public class BlocksResource {
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockInfo.class
implementation = BlockSummaryData.class
)
)
)
@@ -502,7 +501,7 @@ public class BlocksResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public List<BlockInfo> getBlockRange(
public List<BlockSummaryData> getBlockSummaries(
@QueryParam("start") Integer startHeight,
@QueryParam("end") Integer endHeight,
@Parameter(ref = "count") @QueryParam("count") Integer count) {
@@ -515,7 +514,7 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockInfos(startHeight, endHeight, count);
return repository.getBlockRepository().getBlockSummaries(startHeight, endHeight, count);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.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);

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.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;
@@ -155,6 +156,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildTrade(CrossChainBuildRequest tradeRequest) {
Security.checkApiCallAllowed(request);
@@ -250,6 +252,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildTradeMessage(CrossChainTradeRequest tradeRequest) {
Security.checkApiCallAllowed(request);
@@ -333,6 +336,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildRedeemMessage(CrossChainSecretRequest secretRequest) {
Security.checkApiCallAllowed(request);
@@ -404,6 +408,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildCancelMessage(CrossChainCancelRequest cancelRequest) {
Security.checkApiCallAllowed(request);
@@ -459,6 +464,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String deriveP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -485,6 +491,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String deriveP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -542,6 +549,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public CrossChainBitcoinP2SHStatus checkP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -568,6 +576,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public CrossChainBitcoinP2SHStatus checkP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -656,6 +665,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String refundP2shA(CrossChainBitcoinRefundRequest refundRequest) {
Security.checkApiCallAllowed(request);
@@ -683,6 +693,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String refundP2shB(CrossChainBitcoinRefundRequest refundRequest) {
Security.checkApiCallAllowed(request);
@@ -793,6 +804,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String redeemP2shA(CrossChainBitcoinRedeemRequest redeemRequest) {
Security.checkApiCallAllowed(request);
@@ -821,6 +833,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String redeemP2shB(CrossChainBitcoinRedeemRequest redeemRequest) {
Security.checkApiCallAllowed(request);
@@ -935,6 +948,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY})
@SecurityRequirement(name = "apiKey")
public String getBitcoinWalletBalance(String xprv58) {
Security.checkApiCallAllowed(request);
@@ -969,6 +983,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String sendBitcoin(BitcoinSendRequest bitcoinSendRequest) {
Security.checkApiCallAllowed(request);
@@ -1019,6 +1034,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<TradeBotData> getTradeBotStates() {
Security.checkApiCallAllowed(request);
@@ -1049,6 +1065,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
Security.checkApiCallAllowed(request);
@@ -1104,6 +1121,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String tradeBotResponder(TradeBotRespondRequest tradeBotRespondRequest) {
Security.checkApiCallAllowed(request);
@@ -1168,6 +1186,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String tradeBotDelete(String tradePrivateKey58) {
Security.checkApiCallAllowed(request);

View File

@@ -6,8 +6,11 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.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;
}
}
}

View File

@@ -13,9 +13,9 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError;
import org.qortal.api.model.BlockInfo;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
@@ -41,10 +41,10 @@ public class BlocksWebSocket extends ApiWebSocket implements Listener {
return;
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
BlockInfo blockInfo = new BlockInfo(blockData);
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
for (Session session : getSessions())
sendBlockInfo(session, blockInfo);
sendBlockSummary(session, blockSummary);
}
@OnWebSocketConnect
@@ -85,13 +85,13 @@ public class BlocksWebSocket extends ApiWebSocket implements Listener {
return;
}
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
List<BlockSummaryData> blockSummaries = repository.getBlockRepository().getBlockSummaries(height, height);
if (blockSummaries == null || blockSummaries.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
sendBlockInfo(session, blockInfos.get(0));
sendBlockSummary(session, blockSummaries.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
@@ -114,23 +114,23 @@ public class BlocksWebSocket extends ApiWebSocket implements Listener {
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
List<BlockSummaryData> blockSummaries = repository.getBlockRepository().getBlockSummaries(height, height);
if (blockSummaries == null || blockSummaries.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
sendBlockInfo(session, blockInfos.get(0));
sendBlockSummary(session, blockSummaries.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
}
private void sendBlockInfo(Session session, BlockInfo blockInfo) {
private void sendBlockSummary(Session session, BlockSummaryData blockSummary) {
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, blockInfo);
marshall(stringWriter, blockSummary);
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {

View File

@@ -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;
@@ -354,12 +357,8 @@ public class Block {
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
}
byte[] minterSignature;
try {
minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(), minter, encodedOnlineAccounts));
} catch (TransformationException e) {
throw new DataException("Unable to calculate next block minter signature", e);
}
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(),
minter.getPublicKey(), encodedOnlineAccounts));
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
@@ -425,12 +424,8 @@ public class Block {
int version = this.blockData.getVersion();
byte[] reference = this.blockData.getReference();
byte[] minterSignature;
try {
minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(), minter, this.blockData.getEncodedOnlineAccounts()));
} catch (TransformationException e) {
throw new DataException("Unable to calculate next block's minter signature", e);
}
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(),
minter.getPublicKey(), this.blockData.getEncodedOnlineAccounts()));
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
@@ -791,15 +786,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;
@@ -1061,6 +1087,10 @@ public class Block {
// Create repository savepoint here so we can rollback to it after testing transactions
repository.setSavepoint();
if (this.blockData.getHeight() == 212937)
// Apply fix for block 212937 but fix will be rolled back before we exit method
Block212937.processFix(this);
for (Transaction transaction : this.getTransactions()) {
TransactionData transactionData = transaction.getTransactionData();
@@ -1264,6 +1294,10 @@ public class Block {
// Distribute block rewards, including transaction fees, before transactions processed
processBlockRewards();
if (this.blockData.getHeight() == 212937)
// Apply fix for block 212937
Block212937.processFix(this);
}
// We're about to (test-)process a batch of transactions,
@@ -1492,6 +1526,10 @@ public class Block {
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
this.cachedExpandedAccounts = null;
if (this.blockData.getHeight() == 212937)
// Revert fix for block 212937
Block212937.orphanFix(this);
// Block rewards, including transaction fees, removed after transactions undone
orphanBlockRewards();

View File

@@ -0,0 +1,153 @@
package org.qortal.block;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.repository.DataException;
/**
* Block 212937
* <p>
* Somehow a node minted a version of block 212937 that contained one transaction:
* a PAYMENT transaction that attempted to spend more QORT than that account had as QORT balance.
* <p>
* This invalid transaction made block 212937 (rightly) invalid to several nodes,
* which refused to use that block.
* However, it seems there were no other nodes minting an alternative, valid block at that time
* and so the chain stalled for several nodes in the network.
* <p>
* Additionally, the invalid block 212937 affected all new installations, regardless of whether
* they synchronized from scratch (block 1) or used an 'official release' bootstrap.
* <p>
* After lengthy diagnosis, it was discovered that
* the invalid transaction seemed to rely on incorrect balances in a corrupted database.
* Copies of DB files containing the broken chain were also shared around, exacerbating the problem.
* <p>
* There were three options:
* <ol>
* <li>roll back the chain to last known valid block 212936 and re-mint empty blocks to current height</li>
* <li>keep existing chain, but apply database edits at block 212937 to allow current chain to be valid</li>
* <li>attempt to mint an alternative chain, retaining as many valid transactions as possible</li>
* </ol>
* <p>
* Option 1 was highly undesirable due to knock-on effects from wiping 700+ transactions, some of which
* might have affect cross-chain trades, although there were no cross-chain trade completed during
* the decision period.
* <p>
* Option 3 was essentially a slightly better version of option 1 and rejected for similar reasons.
* Attempts at option 3 also rapidly hit cumulative problems with every replacement block due to
* differing block timestamps making some transactions, and then even some blocks themselves, invalid.
* <p>
* This class is the implementation of option 2.
* <p>
* The change in account balances are relatively small, see <tt>block-212937-deltas.json</tt> resource
* for actual values. These values were obtained by exporting the <tt>AccountBalances</tt> table from
* both versions of the database with chain at block 212936, and then comparing. The values were also
* tested by syncing both databases up to block 225500, re-exporting and re-comparing.
* <p>
* The invalid block 212937 signature is: <tt>2J3GVJjv...qavh6KkQ</tt>.
* <p>
* The invalid transaction in block 212937 is:
* <p>
* <code><pre>
{
"amount" : "0.10788294",
"approvalStatus" : "NOT_REQUIRED",
"blockHeight" : 212937,
"creatorAddress" : "QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs",
"fee" : "0.00100000",
"recipient" : "QZi1mNHDbiLvsytxTgxDr9nhJe4pNZaWpw",
"reference" : "J6JukdTVuXZ3JYbHatfZzwxG2vSiZwVCPDzW5K7PsVQKRj8XZeDtqnkGCGGjaSQZ9bQMtV44ky88NnGM4YBQKU6",
"senderPublicKey" : "DBFfbD2M3uh4jPE5PaUcZVvNPfrrJzVB7seeEtBn5SPs",
"signature" : "qkitxdCEEnKt8w6wRfFixtErbXsxWE6zG2ESNhpqBdScikV1WxeA6WZTTMJVV4tCeZdBFXw3V1X5NVztv6LirWK",
"timestamp" : 1607863074904,
"txGroupId" : 0,
"type" : "PAYMENT"
}
</pre></code>
* <p>
* Account <tt>QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs</tt> attempted to spend <tt>0.10888294</tt> (including fees)
* when their QORT balance was really only <tt>0.10886665</tt>.
* <p>
* However, on the broken DB nodes, their balance
* seemed to be <tt>0.10890293</tt> which was sufficient to make the transaction valid.
*/
public final class Block212937 {
private static final Logger LOGGER = LogManager.getLogger(Block212937.class);
private static final String ACCOUNT_DELTAS_SOURCE = "block-212937-deltas.json";
private static final List<AccountBalanceData> accountDeltas = readAccountDeltas();
private Block212937() {
/* Do not instantiate */
}
@SuppressWarnings("unchecked")
private static List<AccountBalanceData> readAccountDeltas() {
Unmarshaller unmarshaller;
try {
// Create JAXB context aware of classes we need to unmarshal
JAXBContext jc = JAXBContextFactory.createContext(new Class[] {
AccountBalanceData.class
}, null);
// Create unmarshaller
unmarshaller = jc.createUnmarshaller();
// Set the unmarshaller media type to JSON
unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json");
// Tell unmarshaller that there's no JSON root element in the JSON input
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
} catch (JAXBException e) {
String message = "Failed to setup unmarshaller to read block 212937 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
ClassLoader classLoader = BlockChain.class.getClassLoader();
InputStream in = classLoader.getResourceAsStream(ACCOUNT_DELTAS_SOURCE);
StreamSource jsonSource = new StreamSource(in);
try {
// Attempt to unmarshal JSON stream to BlockChain config
return (List<AccountBalanceData>) unmarshaller.unmarshal(jsonSource, AccountBalanceData.class).getValue();
} catch (UnmarshalException e) {
String message = "Failed to parse block 212937 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
} catch (JAXBException e) {
String message = "Unexpected JAXB issue while processing block 212937 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
}
public static void processFix(Block block) throws DataException {
block.repository.getAccountRepository().modifyAssetBalances(accountDeltas);
}
public static void orphanFix(Block block) throws DataException {
// Create inverse deltas
List<AccountBalanceData> inverseDeltas = accountDeltas.stream()
.map(delta -> new AccountBalanceData(delta.getAddress(), delta.getAssetId(), 0 - delta.getBalance()))
.collect(Collectors.toList());
block.repository.getAccountRepository().modifyAssetBalances(inverseDeltas);
}
}

View File

@@ -531,7 +531,8 @@ public class BlockChain {
private static void rebuildBlockchain() throws DataException {
// (Re)build repository
RepositoryManager.rebuild();
if (!RepositoryManager.wasPristineAtOpen())
RepositoryManager.rebuild();
try (final Repository repository = RepositoryManager.getRepository()) {
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
@@ -568,7 +569,7 @@ public class BlockChain {
orphanBlockData = repository.getBlockRepository().fromHeight(height);
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onNewBlock(orphanBlockData);
Controller.getInstance().onOrphanedBlock(orphanBlockData);
}
return true;

View File

@@ -18,6 +18,8 @@ public class AtStatesTrimmer implements Runnable {
Thread.currentThread().setName("AT States trimmer");
try (final Repository repository = RepositoryManager.getRepository()) {
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
repository.getATRepository().prepareForAtStateTrimming();
repository.saveChanges();
@@ -30,6 +32,10 @@ public class AtStatesTrimmer implements Runnable {
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();
@@ -37,8 +43,6 @@ public class AtStatesTrimmer implements Runnable {
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);
@@ -49,17 +53,20 @@ public class AtStatesTrimmer implements Runnable {
repository.saveChanges();
if (numAtStatesTrimmed > 0) {
final int finalTrimStartHeight = trimStartHeight;
LOGGER.debug(() -> String.format("Trimmed %d AT state%s between blocks %d and %d",
numAtStatesTrimmed, (numAtStatesTrimmed != 1 ? "s" : ""),
trimStartHeight, upperTrimHeight));
finalTrimStartHeight, upperTrimHeight));
} else {
// Can we move onto next batch?
if (upperTrimmableHeight > upperBatchHeight) {
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
trimStartHeight = upperBatchHeight;
repository.getATRepository().setAtTrimHeight(trimStartHeight);
repository.getATRepository().prepareForAtStateTrimming();
repository.saveChanges();
LOGGER.debug(() -> String.format("Bumping AT state trim height to %d", upperBatchHeight));
final int finalTrimStartHeight = trimStartHeight;
LOGGER.debug(() -> String.format("Bumping AT state base trim height to %d", finalTrimStartHeight));
}
}
}

View File

@@ -16,6 +16,8 @@ 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;
@@ -23,10 +25,15 @@ 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;
@@ -135,9 +142,21 @@ 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
@@ -183,6 +202,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) {
@@ -238,21 +298,36 @@ public class Controller extends Thread {
/** Returns current blockchain height, or 0 if it's not available. */
public int getChainHeight() {
BlockData blockData = this.chainTip;
if (blockData == null)
return 0;
synchronized (this.latestBlocks) {
BlockData blockData = this.latestBlocks.peekLast();
if (blockData == null)
return 0;
return blockData.getHeight();
return blockData.getHeight();
}
}
/** Returns highest block, or null if it's not available. */
public BlockData getChainTip() {
return this.chainTip;
synchronized (this.latestBlocks) {
return this.latestBlocks.peekLast();
}
}
/** Cache new blockchain tip. */
public void setChainTip(BlockData blockData) {
this.chainTip = blockData;
public void refillLatestBlocksCache() throws DataException {
// Set initial chain height/tip
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
synchronized (this.latestBlocks) {
this.latestBlocks.clear();
for (int i = 0; i < BLOCK_CACHE_SIZE && blockData != null; ++i) {
this.latestBlocks.addFirst(blockData);
blockData = repository.getBlockRepository().fromHeight(blockData.getHeight() - 1);
}
}
}
}
public ReentrantLock getBlockchainLock() {
@@ -334,13 +409,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);
@@ -415,6 +485,7 @@ 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());
@@ -460,6 +531,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;
@@ -572,9 +655,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) {
@@ -586,8 +670,6 @@ public class Controller extends Thread {
if (hasStatusChanged)
updateSysTray();
BlockData priorChainTip = this.chainTip;
try {
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
switch (syncResult) {
@@ -656,9 +738,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));
}
@@ -853,11 +932,99 @@ public class Controller extends Thread {
// 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;
@@ -957,11 +1124,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)));
@@ -976,10 +1163,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);
}
@@ -1026,59 +1222,110 @@ public class Controller extends Thread {
private void onNetworkGetBlockSummariesMessage(Peer peer, Message message) {
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
final byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
this.stats.getBlockSummariesStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockSummaryData> blockSummaries = new ArrayList<>();
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
do {
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
if (blockData == null)
// No more blocks to send to peer
break;
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
blockSummaries.add(blockSummary);
parentSignature = blockData.getSignature();
} while (blockSummaries.size() < numberRequested);
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
// If peer's parent signature matches our latest block signature
// then we can short-circuit with an empty response
BlockData chainTip = getChainTip();
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
return;
}
List<BlockSummaryData> blockSummaries = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
blockSummaries = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockSummaryData::new)
.collect(Collectors.toList());
}
if (blockSummaries.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
while (blockData != null && blockSummaries.size() < numberRequested) {
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
blockSummaries.add(blockSummary);
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSummariesStats.cacheHits.incrementAndGet();
if (blockSummaries.size() >= getBlockSummariesMessage.getNumberRequested())
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
}
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
}
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) {
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
byte[] parentSignature = getSignaturesMessage.getParentSignature();
final byte[] parentSignature = getSignaturesMessage.getParentSignature();
this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = new ArrayList<>();
do {
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
if (blockData == null)
// No more signatures to send to peer
break;
parentSignature = blockData.getSignature();
signatures.add(parentSignature);
} while (signatures.size() < getSignaturesMessage.getNumberRequested());
Message signaturesMessage = new SignaturesMessage(signatures);
// If peer's parent signature matches our latest block signature
// then we can short-circuit with an empty response
BlockData chainTip = getChainTip();
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
signaturesMessage.setId(message.getId());
if (!peer.sendMessage(signaturesMessage))
peer.disconnect("failed to send signatures (v2)");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
return;
}
List<byte[]> signatures = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
signatures = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockData::getSignature)
.collect(Collectors.toList());
}
if (signatures.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = getSignaturesMessage.getNumberRequested();
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
while (blockData != null && signatures.size() < numberRequested) {
signatures.add(blockData.getSignature());
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSignaturesV2Stats.cacheHits.incrementAndGet();
if (signatures.size() >= getSignaturesMessage.getNumberRequested())
this.stats.getBlockSignaturesV2Stats.fullyFromCache.incrementAndGet();
}
Message signaturesMessage = new SignaturesMessage(signatures);
signaturesMessage.setId(message.getId());
if (!peer.sendMessage(signaturesMessage))
peer.disconnect("failed to send signatures (v2)");
}
private void onNetworkHeightV2Message(Peer peer, Message message) {
@@ -1689,4 +1936,8 @@ public class Controller extends Thread {
return now - offset;
}
public StatsSnapshot getStatsSnapshot() {
return this.stats;
}
}

View File

@@ -23,6 +23,8 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
// Don't even start trimming until initial rush has ended
Thread.sleep(INITIAL_SLEEP_PERIOD);
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
while (!Controller.isStopping()) {
repository.discardChanges();
@@ -32,12 +34,14 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
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);
@@ -48,16 +52,20 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
repository.saveChanges();
if (numSigsTrimmed > 0) {
final int finalTrimStartHeight = trimStartHeight;
LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
trimStartHeight, upperTrimHeight));
finalTrimStartHeight, upperTrimHeight));
} else {
// Can we move onto next batch?
if (upperTrimmableHeight > upperBatchHeight) {
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
trimStartHeight = upperBatchHeight;
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(trimStartHeight);
repository.saveChanges();
LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
final int finalTrimStartHeight = trimStartHeight;
LOGGER.debug(() -> String.format("Bumping online accounts signatures base trim height to %d", finalTrimStartHeight));
}
}
}

View File

@@ -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,13 +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);
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onNewBlock(orphanBlockData);
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));
@@ -432,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());
@@ -514,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());

View File

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

View File

@@ -3,8 +3,6 @@ package org.qortal.data.block;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.transform.block.BlockTransformer;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockSummaryData {
@@ -14,6 +12,10 @@ public class BlockSummaryData {
private byte[] minterPublicKey;
private int onlineAccountsCount;
// Optional, set during construction
private Long timestamp;
private Integer transactionCount;
// Optional, set after construction
private Integer minterLevel;
@@ -29,17 +31,23 @@ public class BlockSummaryData {
this.onlineAccountsCount = onlineAccountsCount;
}
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount, long timestamp, int transactionCount) {
this.height = height;
this.signature = signature;
this.minterPublicKey = minterPublicKey;
this.onlineAccountsCount = onlineAccountsCount;
this.timestamp = timestamp;
this.transactionCount = transactionCount;
}
public BlockSummaryData(BlockData blockData) {
this.height = blockData.getHeight();
this.signature = blockData.getSignature();
this.minterPublicKey = blockData.getMinterPublicKey();
this.onlineAccountsCount = blockData.getOnlineAccountsCount();
byte[] encodedOnlineAccounts = blockData.getEncodedOnlineAccounts();
if (encodedOnlineAccounts != null) {
this.onlineAccountsCount = BlockTransformer.decodeOnlineAccounts(encodedOnlineAccounts).size();
} else {
this.onlineAccountsCount = 0;
}
this.timestamp = blockData.getTimestamp();
this.transactionCount = blockData.getTransactionCount();
}
// Getters / setters
@@ -60,6 +68,14 @@ public class BlockSummaryData {
return this.onlineAccountsCount;
}
public Long getTimestamp() {
return this.timestamp;
}
public Integer getTransactionCount() {
return this.transactionCount;
}
public Integer getMinterLevel() {
return this.minterLevel;
}

View File

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

View File

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

View File

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

View File

@@ -90,7 +90,10 @@ public interface ATRepository {
/** Returns height of first trimmable AT state. */
public int getAtTrimHeight() throws DataException;
/** Sets new base height for AT state trimming. */
/** 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. */

View File

@@ -2,7 +2,6 @@ package org.qortal.repository;
import java.util.List;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
@@ -139,14 +138,17 @@ public interface BlockRepository {
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException;
/**
* Returns block infos for the passed height range, for API use.
* Returns block summaries for the passed height range, for API use.
*/
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
public List<BlockSummaryData> getBlockSummaries(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. */
/** Sets new base height for trimming online accounts signatures.
* <p>
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
*/
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException;
/**

View File

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

View File

@@ -2,6 +2,8 @@ package org.qortal.repository;
public interface RepositoryFactory {
public boolean wasPristineAtOpen();
public RepositoryFactory reopen() throws DataException;
public Repository getRepository() throws DataException;

View File

@@ -4,10 +4,21 @@ public abstract class RepositoryManager {
private static RepositoryFactory repositoryFactory = null;
public static RepositoryFactory getRepositoryFactory() {
return repositoryFactory;
}
public static void setRepositoryFactory(RepositoryFactory newRepositoryFactory) {
repositoryFactory = newRepositoryFactory;
}
public static boolean wasPristineAtOpen() throws DataException {
if (repositoryFactory == null)
throw new DataException("No repository available");
return repositoryFactory.wasPristineAtOpen();
}
public static Repository getRepository() throws DataException {
if (repositoryFactory == null)
throw new DataException("No repository available");
@@ -35,6 +46,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;

View File

@@ -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);
}
@@ -250,7 +250,8 @@ public class HSQLDBATRepository implements ATRepository {
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
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)) {
@@ -272,10 +273,11 @@ public class HSQLDBATRepository implements ATRepository {
public ATStateData getLatestATState(String atAddress) throws DataException {
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)) {
@@ -306,16 +308,17 @@ public class HSQLDBATRepository implements ATRepository {
+ "CROSS JOIN LATERAL("
+ "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 = ? ");
@@ -337,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");
@@ -371,9 +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 "
+ "LEFT OUTER JOIN ATs USING (AT_address) "
+ "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<>();
@@ -415,45 +419,45 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public void setAtTrimHeight(int trimHeight) throws DataException {
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
// that could result in "transaction rollback: serialization failure"
synchronized (this.repository.trimHeightsLock) {
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
try {
this.repository.executeCheckedUpdate(updateSql, trimHeight);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to set AT state trim height in repository", e);
try {
this.repository.executeCheckedUpdate(updateSql, trimHeight);
this.repository.saveChanges();
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to set AT state trim height in repository", e);
}
}
}
@Override
public void prepareForAtStateTrimming() throws DataException {
// Rebuild cache of latest, non-finished AT states that we can't trim
String dropSql = "DROP TABLE IF EXISTS LatestATStates";
// Rebuild cache of latest AT states that we can't trim
String deleteSql = "DELETE FROM LatestATStates";
try {
this.repository.executeCheckedUpdate(dropSql);
this.repository.executeCheckedUpdate(deleteSql);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to drop temporary latest AT states cache from repository", e);
throw new DataException("Unable to delete temporary latest AT states cache from repository", e);
}
String createSql = "CREATE TEMPORARY TABLE LatestATStates "
+ "AS ("
+ "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"
+ ") "
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"
+ ") "
+ "WITH DATA "
+ "ON COMMIT PRESERVE ROWS";
+ ")";
try {
this.repository.executeCheckedUpdate(createSql);
this.repository.executeCheckedUpdate(insertSql);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to recreate temporary latest AT states cache in repository", e);
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
}
}
@@ -464,13 +468,12 @@ public class HSQLDBATRepository implements ATRepository {
// We're often called so no need to trim all states in one go.
// Limit updates to reduce CPU and memory load.
String sql = "UPDATE ATStates SET state_data = NULL "
+ "WHERE state_data IS NOT NULL "
+ "AND height BETWEEN ? AND ? "
String sql = "DELETE FROM ATStatesData "
+ "WHERE height BETWEEN ? AND ? "
+ "AND NOT EXISTS("
+ "SELECT TRUE FROM LatestATStates "
+ "WHERE LatestATStates.AT_address = ATStates.AT_address "
+ "AND LatestATStates.height = ATStates.height"
+ "WHERE LatestATStates.AT_address = ATStatesData.AT_address "
+ "AND LatestATStates.height = ATStatesData.height"
+ ") "
+ "LIMIT ?";
@@ -488,23 +491,44 @@ public class HSQLDBATRepository implements ATRepository {
if (atStateData.getStateHash() == null || atStateData.getHeight() == null)
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
HSQLDBSaver atStatesSaver = new HSQLDBSaver("ATStates");
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_data", atStateData.getStateData()).bind("state_hash", atStateData.getStateHash())
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 {
saveHelper.execute(this.repository);
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);
}
@@ -514,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);
}

View File

@@ -6,7 +6,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
@@ -355,7 +354,8 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
String sql = "SELECT signature, height, minter, online_accounts_count FROM Blocks WHERE height BETWEEN ? AND ?";
String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count "
+ "FROM Blocks WHERE height BETWEEN ? AND ?";
List<BlockSummaryData> blockSummaries = new ArrayList<>();
@@ -368,8 +368,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
int height = resultSet.getInt(2);
byte[] minterPublicKey = resultSet.getBytes(3);
int onlineAccountsCount = resultSet.getInt(4);
long timestamp = resultSet.getLong(5);
int transactionCount = resultSet.getInt(6);
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount);
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
timestamp, transactionCount);
blockSummaries.add(blockSummary);
} while (resultSet.next());
@@ -380,11 +383,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
@Override
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
public List<BlockSummaryData> getBlockSummaries(Integer startHeight, Integer endHeight, Integer count) throws DataException {
StringBuilder sql = new StringBuilder(512);
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
sql.append("SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count ");
/*
* start end count result
@@ -401,7 +404,6 @@ public class HSQLDBBlockRepository implements BlockRepository {
if (startHeight != null && endHeight != null) {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ? AND ?");
bindParams.add(startHeight);
bindParams.add(Integer.valueOf(endHeight - 1));
@@ -413,11 +415,9 @@ public class HSQLDBBlockRepository implements BlockRepository {
if (endHeight == null) {
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
sql.append("JOIN Blocks ON height BETWEEN (max_height - ? + 1) AND max_height ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
bindParams.add(count);
} else {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ? AND ?");
bindParams.add(Integer.valueOf(endHeight - count));
bindParams.add(Integer.valueOf(endHeight - 1));
@@ -432,32 +432,33 @@ public class HSQLDBBlockRepository implements BlockRepository {
count = 50;
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ? AND ?");
bindParams.add(startHeight);
bindParams.add(Integer.valueOf(startHeight + count - 1));
}
List<BlockInfo> blockInfos = new ArrayList<>();
List<BlockSummaryData> blockSummaries = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return blockInfos;
return blockSummaries;
do {
byte[] signature = resultSet.getBytes(1);
int height = resultSet.getInt(2);
long timestamp = resultSet.getLong(3);
int transactionCount = resultSet.getInt(4);
String minterAddress = resultSet.getString(5);
byte[] minterPublicKey = resultSet.getBytes(3);
int onlineAccountsCount = resultSet.getInt(4);
long timestamp = resultSet.getLong(5);
int transactionCount = resultSet.getInt(6);
BlockInfo blockInfo = new BlockInfo(signature, height, timestamp, transactionCount, minterAddress);
blockInfos.add(blockInfo);
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
timestamp, transactionCount);
blockSummaries.add(blockSummary);
} while (resultSet.next());
return blockInfos;
return blockSummaries;
} catch (SQLException e) {
throw new DataException("Unable to fetch height-ranged block infos from repository", e);
throw new DataException("Unable to fetch height-ranged block summaries from repository", e);
}
}
@@ -477,13 +478,18 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException {
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
// that could result in "transaction rollback: serialization failure"
synchronized (this.repository.trimHeightsLock) {
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
try {
this.repository.executeCheckedUpdate(updateSql, trimHeight);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
try {
this.repository.executeCheckedUpdate(updateSql, trimHeight);
this.repository.saveChanges();
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
}
}
}

View File

@@ -18,11 +18,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;
}
/**
@@ -40,23 +45,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;
}
/**
@@ -65,7 +68,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()) {
@@ -660,9 +663,10 @@ 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)");
// stmt.execute("ALTER TABLE ATStates DROP created_when");
// stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
break;
case 26:
@@ -677,6 +681,96 @@ public class HSQLDBDatabaseUpdates {
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;
default:
// nothing to do
return false;

View File

@@ -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;
@@ -59,6 +60,8 @@ public class HSQLDBRepository implements Repository {
protected List<String> sqlStatements;
protected long sessionId;
protected final Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
// We want the same object corresponding to the actual DB
protected final Object trimHeightsLock = RepositoryManager.getRepositoryFactory();
private final ATRepository atRepository = new HSQLDBATRepository(this);
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
@@ -183,8 +186,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 {
@@ -208,7 +223,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();
@@ -298,11 +313,12 @@ 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
@@ -342,11 +358,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) {
@@ -355,12 +372,21 @@ public class HSQLDBRepository implements Repository {
// Actually create backup
try (Statement stmt = this.connection.createStatement()) {
stmt.execute("BACKUP DATABASE TO 'backup/' NOT BLOCKING AS FILES");
stmt.execute("BACKUP DATABASE TO 'backup/' BLOCKING AS FILES");
} catch (SQLException e) {
throw new DataException("Unable to backup 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!
@@ -374,8 +400,34 @@ public class HSQLDBRepository implements 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);
@@ -411,11 +463,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).
@@ -455,6 +508,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,
@@ -504,7 +561,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();
}
@@ -597,7 +654,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();
}
@@ -791,15 +848,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();
@@ -833,14 +890,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;
}
@@ -848,7 +910,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();
}
}

View File

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

View File

@@ -65,6 +65,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
@@ -83,6 +84,10 @@ 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
@@ -271,6 +276,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
@@ -319,6 +327,10 @@ public class Settings {
return !BlockChain.getInstance().isTestChain();
}
public String getApiKey() {
return this.apiKey;
}
public boolean isApiLoggingEnabled() {
return this.apiLoggingEnabled;
}
@@ -430,6 +442,14 @@ public class Settings {
return this.showBackupNotification;
}
public long getRepositoryCheckpointInterval() {
return this.repositoryCheckpointInterval;
}
public boolean getShowCheckpointNotification() {
return this.showCheckpointNotification;
}
public long getAtStatesMaxLifetime() {
return this.atStatesMaxLifetime;
}

View File

@@ -7,7 +7,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.data.at.ATStateData;
@@ -23,7 +22,6 @@ import org.qortal.utils.Base58;
import org.qortal.utils.Serialization;
import org.qortal.utils.Triple;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
@@ -334,27 +332,20 @@ public class BlockTransformer extends Transformer {
public static byte[] getBytesForMinterSignature(BlockData blockData) throws TransformationException {
byte[] minterSignature = getMinterSignatureFromReference(blockData.getReference());
PublicKeyAccount minter = new PublicKeyAccount(null, blockData.getMinterPublicKey());
return getBytesForMinterSignature(minterSignature, minter, blockData.getEncodedOnlineAccounts());
return getBytesForMinterSignature(minterSignature, blockData.getMinterPublicKey(), blockData.getEncodedOnlineAccounts());
}
public static byte[] getBytesForMinterSignature(byte[] minterSignature, PublicKeyAccount minter, byte[] encodedOnlineAccounts)
throws TransformationException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(MINTER_SIGNATURE_LENGTH + MINTER_PUBLIC_KEY_LENGTH + encodedOnlineAccounts.length);
public static byte[] getBytesForMinterSignature(byte[] minterSignature, byte[] minterPublicKey, byte[] encodedOnlineAccounts) {
byte[] bytes = new byte[MINTER_SIGNATURE_LENGTH + MINTER_PUBLIC_KEY_LENGTH + encodedOnlineAccounts.length];
bytes.write(minterSignature);
System.arraycopy(minterSignature, 0, bytes, 0, MINTER_SIGNATURE_LENGTH);
// We're padding here just in case the minter is the genesis account whose public key is only 8 bytes long.
bytes.write(Bytes.ensureCapacity(minter.getPublicKey(), MINTER_PUBLIC_KEY_LENGTH, 0));
System.arraycopy(minterPublicKey, 0, bytes, MINTER_SIGNATURE_LENGTH, MINTER_PUBLIC_KEY_LENGTH);
bytes.write(encodedOnlineAccounts);
System.arraycopy(encodedOnlineAccounts, 0, bytes, MINTER_SIGNATURE_LENGTH + MINTER_PUBLIC_KEY_LENGTH, encodedOnlineAccounts.length);
return bytes.toByteArray();
} catch (IOException e) {
throw new TransformationException(e);
}
return bytes;
}
public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException {

View File

@@ -0,0 +1,719 @@
[
{ "address": "Qa43JP9hnNjfSy1f3LYYNFhhSuMokUoYqQ", "assetId": 0, "balance": 0.00003628},
{ "address": "Qa4VMxDhmGH5dgYLiuFSyaWju8xb2fGZhs", "assetId": 0, "balance": -0.00010321},
{ "address": "Qa8pRawmcviX1BHQpNCt4vBYHz7HjdNfkL", "assetId": 0, "balance": -0.00010321},
{ "address": "QacFHzkV265jd57jfTZ5gSuW8dj4W1ttYs", "assetId": 0, "balance": 0.00003628},
{ "address": "QadVAZb3yc78yjQwQDJ8bXCvFohAdEovu7", "assetId": 0, "balance": -0.00010321},
{ "address": "QaeFXCfi73Wptwve5R2RdFSUJUs2dqsHXY", "assetId": 0, "balance": 0.00003628},
{ "address": "Qaf5BHpXrWKK3dprQX7zcCXtCPiQdhm7oo", "assetId": 0, "balance": 0.00001276},
{ "address": "QaK6URQq4vwEDWyBtmS25kor49Z56An7xn", "assetId": 0, "balance": -0.00010321},
{ "address": "QakzYX9JZyUjRYtXJeaQbirXTuUqMFdp7d", "assetId": 0, "balance": -0.00010321},
{ "address": "QaLWoAkjc7ip5Y38p5FX8vVbEYFHCz2zHh", "assetId": 0, "balance": -0.00010321},
{ "address": "QanYq81HNrintpSE6FPRVioHfhCHzTkN83", "assetId": 0, "balance": -0.00010321},
{ "address": "QaoVMdbpPQDHfBAar1HfPoEBzDoFa2PjS8", "assetId": 0, "balance": -0.00010321},
{ "address": "QapE6pVuceYdVKnHVePbeaqf5QNeoDKbqr", "assetId": 0, "balance": 0.00001276},
{ "address": "QaPKuyyQtXJcsVhKLKgxCcYewxwaawxLrB", "assetId": 0, "balance": 0.00000011},
{ "address": "QaPKuyyQtXJcsVhKLKgxCcYewxwaawxLrB", "assetId": 2, "balance": 0.00000011},
{ "address": "Qaq4sV9wkdSSSgJtaPuuSV15tqsVPD78rz", "assetId": 0, "balance": 0.00003628},
{ "address": "Qar2d2pXBP7NCe8mNTbzTSzAQ48ptGqrMC", "assetId": 0, "balance": 0.00003628},
{ "address": "QasrD9TxGAuWqnRpJxBBwwh7Nj6BHiA77d", "assetId": 0, "balance": 0.00003628},
{ "address": "QaSXJSHbQ4xmwaQd5tKJMAqvVzwzLvhddP", "assetId": 0, "balance": 0.00003628},
{ "address": "Qata5oApMShnD4F1kcgSJMTiYsxTPSFW4F", "assetId": 0, "balance": 0.00001283},
{ "address": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "assetId": 0, "balance": 0.00000011},
{ "address": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "assetId": 2, "balance": 0.00000011},
{ "address": "QavBkY3kRPJxtvsU5yWuhUnMdDWvs4E3Dw", "assetId": 0, "balance": 0.00003628},
{ "address": "QavzMF32Xvbg4Q4rM9a9R5WVmQZt7iW4Fa", "assetId": 0, "balance": 0.00003628},
{ "address": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "assetId": 0, "balance": 0.00000015},
{ "address": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "assetId": 2, "balance": 0.00000004},
{ "address": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "assetId": 0, "balance": 0.00003628},
{ "address": "QaxMTV7fGSnibyvVjRaX7FRerrb9aMW6SR", "assetId": 0, "balance": 0.00003628},
{ "address": "QaywytB5dqQoDgBejmEhWXWaDgfL1CRBGH", "assetId": 0, "balance": 0.00003628},
{ "address": "QaZs97g4Mbq9tXMoBWbhw3jFvBBVkWKS5F", "assetId": 0, "balance": 0.00003628},
{ "address": "Qb1pkXG4xufNS3ki354CWkEmC1gmz6D2H7", "assetId": 0, "balance": 0.00003628},
{ "address": "Qb2VbWdrY2E9uLALmyan35E6H5ze6tBmxX", "assetId": 0, "balance": 0.00003628},
{ "address": "Qb5F6KX4Fg1LRM21QJF48m1EYxnipFfRy1", "assetId": 0, "balance": 0.00003628},
{ "address": "Qb9Ycc3f6KUyWPMBGgeEczy4HorPFfy2hj", "assetId": 0, "balance": -0.00010321},
{ "address": "QbBMsJvjo4ZouPPGegxRs5kqKuRzfLRYMU", "assetId": 0, "balance": 0.00001276},
{ "address": "QbchhqR3QLE1T1kRzySFWsVamhPy8oyeC1", "assetId": 0, "balance": -0.00010321},
{ "address": "Qbcy4uyMkQF2JXYqGkueDiFNZ4tHjRg8CR", "assetId": 0, "balance": 0.00000011},
{ "address": "QbdV2vipqMui1eQnj9ZudQgw4e8zRgQ9Lk", "assetId": 0, "balance": -0.00010321},
{ "address": "Qbh6gWsxNtcaKq5sAq4NVkCTXuqyA6pbUm", "assetId": 0, "balance": 0.00003628},
{ "address": "QbJhMqYk94FExie11Vs5y5x7CNUS5e5b5W", "assetId": 0, "balance": -0.00010321},
{ "address": "QbJJho6sTHnqL2ECivtfUrYZTuEgemEha2", "assetId": 0, "balance": 0.00003628},
{ "address": "QbJqEntoBFps7XECQkTDFzXNCdz9R2qmkB", "assetId": 0, "balance": 0.00000103},
{ "address": "QbLPi1Ac6zZGTLURapA6YxyiFYVXH6uZYQ", "assetId": 0, "balance": -0.00010321},
{ "address": "QbmJDoAJ9cjRNM9AuMv5AZc4w83kqosCYS", "assetId": 0, "balance": 0.00000004},
{ "address": "QbMppCoLPnXdzBQBCqXaD1iCBLGKVSW7Z1", "assetId": 0, "balance": -0.00010321},
{ "address": "QbNaKP3udSoKqgRdVR5uik5tb2QrgmyY5w", "assetId": 0, "balance": -0.00010321},
{ "address": "QbnizezPemhpQg1roAMs9MAvJVw4KHiSuq", "assetId": 0, "balance": -0.00010321},
{ "address": "Qbp8CZLnBwphPGwqJGm91LTaFJ4mkXZLgg", "assetId": 0, "balance": -0.00010321},
{ "address": "QbPGzVLU7B9TSrr3fde2u7xoyQfRUpk8st", "assetId": 0, "balance": 0.00003628},
{ "address": "Qbq1ctcvwnkChPmU9PiH4fAExbgTv3fBm3", "assetId": 0, "balance": 0.00003628},
{ "address": "QbQQhQM7XdoGPPjJG1ffNwGEU4tmgUVSyb", "assetId": 0, "balance": 0.00003628},
{ "address": "QbqRyFw7Xu6Nsb4FraaUSe7nUPukuUpekG", "assetId": 0, "balance": 0.00000011},
{ "address": "QbSLbuAxRMqe9vdQpmhbannkJcieXgPfnY", "assetId": 0, "balance": 0.00003628},
{ "address": "Qbtut4Z8a37Mokd5uvsA54WXfBHN1Ho1Kx", "assetId": 0, "balance": -0.00006713},
{ "address": "QbUPbjTu3NpEZQcJp4JcTarLRon4oTiSqi", "assetId": 0, "balance": 0.00003628},
{ "address": "QbUSTCbTkdKgaMJuKiEfJfa32cXTy4sHkr", "assetId": 0, "balance": -0.00010321},
{ "address": "QbvxC3ENqomXp11833APchdjeyCNd49nLj", "assetId": 0, "balance": 0.00000011},
{ "address": "QbXjh5buBXW68AmJBUW2URV4YnM59vMhkP", "assetId": 0, "balance": 0.00003628},
{ "address": "QbxJvwrEHZs7MDE8rbqBwZAZkcywue5F3W", "assetId": 0, "balance": 0.00003628},
{ "address": "QbY4qRTuHee7gX93n7RJytxNXJhMeQcdCP", "assetId": 0, "balance": -0.00010321},
{ "address": "QbYaYDYjTDohUtsbALeR4PQPUXL2qYe3hh", "assetId": 0, "balance": 0.00003628},
{ "address": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "assetId": 0, "balance": 0.00003628},
{ "address": "QbYVbsJ99wWEDNn7fGNgUYuSN1fk6y3T1x", "assetId": 0, "balance": 0.00003628},
{ "address": "QbyVFRE1zKKcprNvpCBx1VfEh9uosYZojs", "assetId": 0, "balance": 0.00003628},
{ "address": "QbzERVYhUEJKvWRVpEeiacV9HcNxjoCzA4", "assetId": 0, "balance": 0.00000011},
{ "address": "Qc1wMMJbivCnM4QjvgJDWqmSQYUfuswhts", "assetId": 0, "balance": 0.00003628},
{ "address": "Qc54dt4km6NrxBMvtEX51jKiuNmHmzEuee", "assetId": 0, "balance": 0.00003628},
{ "address": "Qc9dZchoYfc1eRJhSLXR9rxSHcqNB47Dex", "assetId": 0, "balance": 0.00000026},
{ "address": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "assetId": 0, "balance": 0.00003628},
{ "address": "QcCL2sk1nLLE99HgqjGpqLQbCwPtSozBx5", "assetId": 0, "balance": -0.00010321},
{ "address": "Qce2Djqrk2WzG1QhMZ3BqFok9HGsz4wtM3", "assetId": 0, "balance": 0.00003628},
{ "address": "QcEpMZ9NUkLcEv2aWw6FPu9f58CSKVSH8N", "assetId": 0, "balance": 0.00000011},
{ "address": "Qcf5wVLGjgdt57kYsn1D5H6TuWorqb6hww", "assetId": 0, "balance": -0.00010321},
{ "address": "QcfhhsQG9vgVdQULu8RrXaXooJUec1xMj1", "assetId": 0, "balance": -0.00010321},
{ "address": "QcFZ9yCvGESF7gi12jt9XF8RY423c6RfLm", "assetId": 0, "balance": -0.00010321},
{ "address": "QcGdwubfgY14EPjc7peuWn7vz8tKZbWQw4", "assetId": 0, "balance": 0.00003628},
{ "address": "QcGFjReZ7yjNJaCMF1SfXbdrPCGeZhdgCv", "assetId": 0, "balance": 0.00003628},
{ "address": "QcgxbHijQFUaBBD1Xv6k2Fjh2hRPp2AUFg", "assetId": 0, "balance": 0.00003628},
{ "address": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "assetId": 0, "balance": 0.00000004},
{ "address": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "assetId": 2, "balance": 0.00000004},
{ "address": "QcJwVCyzraPy51uB4xd4f94n2UFYAsznGC", "assetId": 0, "balance": 0.00003628},
{ "address": "Qck27pE28zWwmMoxa2hypGa9X6rhjBBfmJ", "assetId": 0, "balance": 0.00001276},
{ "address": "QckLxn2NgwZZjV92W8VKnHUWiWUVmQrhiJ", "assetId": 0, "balance": -0.00001256},
{ "address": "QcNmqT8CZ6zSZwuRm5LahRZnuGBJRnPY8o", "assetId": 0, "balance": 0.00000011},
{ "address": "QcPPNyDKGk5vQfPEpXQQKvYeidvBZ9T7nS", "assetId": 0, "balance": 0.00003628},
{ "address": "QcPro2T97Q8cAfcVM4Pn4fv71Za4T6oeFD", "assetId": 0, "balance": 0.00000103},
{ "address": "QcQPQmeU1hw8fWQmsBCKEuxg3kRizaQYUz", "assetId": 0, "balance": 0.00003628},
{ "address": "QcqTj1unHj5FXExKu7RpRJHPBMPjGtmXtJ", "assetId": 0, "balance": 0.00003628},
{ "address": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "assetId": 0, "balance": 0.00000019},
{ "address": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "assetId": 2, "balance": 0.00000015},
{ "address": "QcRYGiF4ffxMUq3CGNrcFP646KbeCcnK66", "assetId": 0, "balance": 0.00003628},
{ "address": "QcU4VhU9ohDXU4k4AUMapgJRYSzEpizjLN", "assetId": 0, "balance": 0.00000011},
{ "address": "QcUA6GT9FiPBbeE7ttBXu1avBHZzDsZg2o", "assetId": 0, "balance": 0.00003628},
{ "address": "QcwEgPdsvF1TugqnHvT2bwXLCLzEKMVk3A", "assetId": 0, "balance": 0.00003628},
{ "address": "Qcx4PE9bn3qXn88XhpDmNSBGS32SmDE8Ds", "assetId": 0, "balance": 0.00002352},
{ "address": "QcXuXDcqzq8goJAqwambRU2Uk9RQ513mV9", "assetId": 0, "balance": -0.00003608},
{ "address": "QcyBacxzvdMP5votSnAJyA39fu9BgYhWmG", "assetId": 0, "balance": 0.00003628},
{ "address": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "assetId": 0, "balance": 0.00003628},
{ "address": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "assetId": 0, "balance": 0.00000011},
{ "address": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "assetId": 2, "balance": 0.00000011},
{ "address": "Qd33zAmKqm89UWMes6bfRMMoSNjasehzzX", "assetId": 0, "balance": -0.00010321},
{ "address": "Qd3bVidnA4fhKv1xwHcKsDZC3MUBFhkrUa", "assetId": 0, "balance": 0.00003628},
{ "address": "Qd453ewoyESrEgUab6dTFe2pufWkD94Tsm", "assetId": 0, "balance": 0.00000103},
{ "address": "Qd75TjafsrikBgnfA6Hb6Y9wk45LwiavB1", "assetId": 0, "balance": -0.00010321},
{ "address": "Qd7rmD8PZvKKyJLr4qgvFQzeLPRhYkKcya", "assetId": 0, "balance": -0.00001256},
{ "address": "Qdb27GFXfyWFDKY3urtrKrQRshkeL8hWgt", "assetId": 0, "balance": 0.00003628},
{ "address": "QdFZk74skMUu4rKMPEmcSVwR87LNDe6o3Y", "assetId": 0, "balance": 0.00003628},
{ "address": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "assetId": 0, "balance": 0.00000011},
{ "address": "QdgbtYSRsDgKZ2PZMKCfoNWfGuvDm2idmP", "assetId": 0, "balance": -0.00010321},
{ "address": "QdHc49iRMiCaanZfD8kGiTZaZxJneDTU7j", "assetId": 0, "balance": -0.00010321},
{ "address": "QdkheUawBwuvhD5J5N21uqypH1hZw1enGE", "assetId": 0, "balance": 0.00003628},
{ "address": "QdkTGqDYde3Y9Q6EgxmBrJGAK2jm4HXspX", "assetId": 0, "balance": 0.00003628},
{ "address": "QdmFGD2ef9gdkUbXpNBuKVkbGGnBRBoReS", "assetId": 0, "balance": -0.00010321},
{ "address": "QdP6twdTsJpq3eLDgi83t6LH367gauJLqo", "assetId": 0, "balance": -0.00010321},
{ "address": "QdRzjsQrz8edqeRNX7VcASbSz2hPfvX783", "assetId": 0, "balance": -0.00010321},
{ "address": "QdsakiJEhKaKGtG4ue2k5xJdt6kYsxwPba", "assetId": 0, "balance": 0.00003628},
{ "address": "QdsiBFfTQUPMrS6NuWdcYCU94t8M64EcPf", "assetId": 0, "balance": -0.00010321},
{ "address": "QdsMQUyuWyYT5Sit8YSMW9bKjBhfq8MwRY", "assetId": 0, "balance": -0.00010321},
{ "address": "QdSQjxAdRwfg4JgdaXNp5CZwTFL8ARwDJf", "assetId": 0, "balance": -0.00010321},
{ "address": "QdtAQm1EGNgM7QDSaC2qvV9WdpRHwpApUT", "assetId": 0, "balance": 0.00000011},
{ "address": "QdTY1v63aMSibfPV2sJTAJZu2mqDP4dMZV", "assetId": 0, "balance": -0.00010321},
{ "address": "QdudYG9SDw5WYzfoj9oq3QC4abHx8ZWCce", "assetId": 0, "balance": 0.00003628},
{ "address": "QdwSxr3t4hdGHjQFy6EVGR9yGMipefsTuo", "assetId": 0, "balance": 0.00003628},
{ "address": "QdxDaHTEX5cUg4S6ohMAi6mE8wQZCqQBoT", "assetId": 0, "balance": 0.00003628},
{ "address": "QdXdUxnyKGGo7eEfTcx85oEikNe5nYnuwa", "assetId": 0, "balance": 0.00003628},
{ "address": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "assetId": 0, "balance": 0.00000011},
{ "address": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "assetId": 2, "balance": 0.00000011},
{ "address": "QdXMNPHhbt2kiwkp7NPskBsrZudxZ6gXN2", "assetId": 0, "balance": 0.00003628},
{ "address": "QdyzBSPBNLfyCfdPNBn86EcXUZeDdkCYLm", "assetId": 0, "balance": 0.00096104},
{ "address": "QdZ5Krd84CX6oh6jorEhYyL7zdCacrPAj2", "assetId": 0, "balance": 0.00003628},
{ "address": "Qe29bjnmk29z19Nw3xBkbWMqMzy7SkzZ57", "assetId": 0, "balance": 0.00003628},
{ "address": "Qe7RxFfsV5JNkQNuK9UVvtTQMXhcCcTTTf", "assetId": 0, "balance": -0.00010321},
{ "address": "Qe9S8zA27FPdPVVLcVQj9noaKsuwySPKdq", "assetId": 0, "balance": -0.00010321},
{ "address": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "assetId": 0, "balance": 0.00000019},
{ "address": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "assetId": 2, "balance": 0.00000015},
{ "address": "QeAa7yawpJqQYk7PNisVD89HezskBRecH6", "assetId": 0, "balance": 0.00003628},
{ "address": "QeaDGU85fpffwsw9ngmd98QsT6NaFyFFed", "assetId": 0, "balance": 0.00000011},
{ "address": "QeaDGU85fpffwsw9ngmd98QsT6NaFyFFed", "assetId": 2, "balance": 0.00000011},
{ "address": "QeAHiq28seiCm7wxMoo4NWJtAoBVMtZrpc", "assetId": 0, "balance": 0.00003628},
{ "address": "QeaJHCCTy7AebbPeF1scsBLbezcBHAKtKt", "assetId": 0, "balance": 0.00001276},
{ "address": "QeAwxFMkYMmTJN5dysZtYAaq2SAxjeYrL4", "assetId": 0, "balance": 0.00001276},
{ "address": "Qec83tt9eX6Ng41GE8PU91GWMi72Hk74K5", "assetId": 0, "balance": 0.00003628},
{ "address": "QeCTKHwG4zypj1bV7uNAzyMc4hwed2EFga", "assetId": 0, "balance": 0.00001276},
{ "address": "QeH2ajmr3ca3t2g6xcnbmFeGYd9BeACvA8", "assetId": 0, "balance": 0.00001276},
{ "address": "QekAWuiw9PUQfygRRF31aLemzHBLkRWpiU", "assetId": 0, "balance": -0.00010321},
{ "address": "QepkE9dJdWsYZZdYRP5bV4NbzQnpABWR4m", "assetId": 0, "balance": -0.00010321},
{ "address": "Qeq85FoJpxtzoDM93WiNQQCXiuiFynRQzm", "assetId": 0, "balance": 0.00000011},
{ "address": "Qeq85FoJpxtzoDM93WiNQQCXiuiFynRQzm", "assetId": 2, "balance": 0.00000011},
{ "address": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "assetId": 0, "balance": 0.00000033},
{ "address": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "assetId": 2, "balance": 0.00000033},
{ "address": "QesUoX7rrugxqGFCk4AYntQkoxvXcLpEoS", "assetId": 0, "balance": 0.00003628},
{ "address": "QeSzLpUw9as4LUHTJ3CNK6SW9okCkU1qMG", "assetId": 0, "balance": 0.00003628},
{ "address": "QeTgFSAQj6AihCoJ5cfNJt2ZCDkSUGSAnB", "assetId": 0, "balance": -0.00010321},
{ "address": "QeU4z63x84mZJmwjxZLKWkJbRu46iP1H2z", "assetId": 0, "balance": -0.00010321},
{ "address": "QeUE2MQKGopfmrcLknKfrDnJ8ddoktxrHr", "assetId": 0, "balance": -0.00010321},
{ "address": "QeVGUYQSiVgAq4mZ2KQswbkkxBxxH3jb9Q", "assetId": 0, "balance": 0.00003628},
{ "address": "QewrEYLQ7anM7UyPvLEEitQpfD4pjt1pQQ", "assetId": 0, "balance": -0.00010321},
{ "address": "QeY5cbodjaunb2anyhQMwurmZtZUuuDCc1", "assetId": 0, "balance": 0.00003628},
{ "address": "QeZm1XBbyycGh8mdcoBTGpD2Z4v5unA2yt", "assetId": 0, "balance": 0.00003628},
{ "address": "QezTNFB9czsSYhbJN9YMLNrhRNtmazJrfC", "assetId": 0, "balance": 0.00003628},
{ "address": "Qf2eC5PFMzfqdcS5xq4vfE4n8Hmt3iuYuk", "assetId": 0, "balance": -0.00010321},
{ "address": "Qf3gdKYQqKgXs6sAVkyW2uHkautxNbzgvJ", "assetId": 0, "balance": 0.00001276},
{ "address": "Qf4QdaFNUN7m3eKJfst1vzq87n3cgER6gT", "assetId": 0, "balance": -0.00010321},
{ "address": "Qf9AMghvwAMwoRi38YzDNNxQHrEwAwgyNo", "assetId": 0, "balance": 0.00003628},
{ "address": "QfbX8JJupEw5ckNtU4upQgET35oLTr5e6v", "assetId": 0, "balance": 0.00003628},
{ "address": "Qfd3gxberH9K6ipiV33VH3TUooNFYyV1iu", "assetId": 0, "balance": 0.00003628},
{ "address": "QfdMLMLZC9Kt15DRAdxwNfaYdGBEMF9Sb4", "assetId": 0, "balance": 0.00003628},
{ "address": "Qffd1qyWXmC5ZgUc4GQzPCHkeH29DS52H4", "assetId": 0, "balance": 0.00003628},
{ "address": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "assetId": 0, "balance": 0.00000037},
{ "address": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "assetId": 2, "balance": 0.00000037},
{ "address": "QfjL32jLsxtumbfx6ufmfCFCBccVCQFkrh", "assetId": 0, "balance": 0.00000011},
{ "address": "QfJVbN5dRnMUSedZH68HhYCTJzjbUo121Q", "assetId": 0, "balance": 0.00003628},
{ "address": "QfKsnQFFJWMkXEz2bdSuB734uMNpi5VAaD", "assetId": 0, "balance": -0.00010321},
{ "address": "QfmM8dgfikTB2FYVuJ9owzQXVm8wP7T4QT", "assetId": 0, "balance": 0.00000014},
{ "address": "QfnbnWrRQ4HNDQvtg3wG2B1eC4ycUsFqZz", "assetId": 0, "balance": 0.00003628},
{ "address": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "assetId": 0, "balance": 0.00000011},
{ "address": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "assetId": 2, "balance": 0.00000011},
{ "address": "QfpUpwgV5h6SQqaywGvxvBzzV9D774993x", "assetId": 0, "balance": 0.00003628},
{ "address": "Qfr6suiRoJVGWgmxrAb5sdZVWWuPm1aXqD", "assetId": 0, "balance": -0.00010321},
{ "address": "QfRykgxR139CmZ4nDjmFQvaSmNiv576ZYT", "assetId": 0, "balance": 0.00003628},
{ "address": "Qft1Ckorj3uckvuTjokt6k2FB15oNkVW5a", "assetId": 0, "balance": 0.00001276},
{ "address": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "assetId": 0, "balance": 0.00000022},
{ "address": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "assetId": 2, "balance": 0.00000022},
{ "address": "QfTxdUv4M5LWuaoybQwh1VZ8843Wqq1r1t", "assetId": 0, "balance": 0.00003628},
{ "address": "QfUu2KAEuoxBKHMNFMKaryyoX7vRTSvCFP", "assetId": 0, "balance": 0.00003628},
{ "address": "Qfw8iJLRok3vFrenNxt2o4DatGY3hThsmr", "assetId": 0, "balance": -0.00010321},
{ "address": "QfWuDf4QE2ygs3mT1nokqqgQgFfgYm2BMo", "assetId": 0, "balance": -0.00010321},
{ "address": "Qfxkjavp3UR7tuG988Hau1PF3Um27fU6VX", "assetId": 0, "balance": 0.00003628},
{ "address": "Qg1yzP82SghJT7o3kgcWMMF6UYYb6BS8c1", "assetId": 0, "balance": 0.00003628},
{ "address": "Qg2273uygJvXdtp2kmjWgPEy48nX56ctZj", "assetId": 0, "balance": 0.00003628},
{ "address": "Qg32vHmYCUq16co8mj4Ljb8bWzWu2eWmyF", "assetId": 0, "balance": -0.00010321},
{ "address": "Qg6B3mCqHBUY6jm6fL2ddtUvWTZea3xcfV", "assetId": 0, "balance": -0.00003608},
{ "address": "Qg7TPhUD5sns2pJiLjxnktRAx71WXsNp9y", "assetId": 0, "balance": 0.00003628},
{ "address": "Qg9d4zDLvzhqqjDvRz99jiFoRnygTyzeZg", "assetId": 0, "balance": 0.00003628},
{ "address": "Qga1LWYfbXkBaNm5LCh4HwSW8L3g8MFzSm", "assetId": 0, "balance": 0.00003628},
{ "address": "QgBfw49fpZzCL7FRLwMsq8677ffZLk1XBa", "assetId": 0, "balance": 0.00003628},
{ "address": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "assetId": 0, "balance": 0.00003628},
{ "address": "QgCQq4cFaGrJhwvKs4XwvccKiLZ8GVMCXR", "assetId": 0, "balance": 0.00000011},
{ "address": "QgECFJiiri2dDN4zA32URvbdDid2cFrJwM", "assetId": 0, "balance": 0.00000051},
{ "address": "QgECFJiiri2dDN4zA32URvbdDid2cFrJwM", "assetId": 2, "balance": 0.00000011},
{ "address": "QgEGaSaoCj1bGxyj35qaZcpb23Px2bBJmq", "assetId": 0, "balance": -0.00010321},
{ "address": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "assetId": 0, "balance": 0.00003628},
{ "address": "Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", "assetId": 0, "balance": 0.00000103},
{ "address": "QgFjfyApbjupAa1PLBdy5NGNZWXEha1p9T", "assetId": 0, "balance": -0.00010321},
{ "address": "QggJ9Rnh99rRJMjsp6nLSuR4m8FAve8Wfe", "assetId": 0, "balance": -0.00010321},
{ "address": "QgHhee3CSRavmr7h87XSsLb3esiQUyRjxj", "assetId": 0, "balance": -0.00010321},
{ "address": "QgHRqFHrhTDL4TmnzXpQDtno4q24Q24uL9", "assetId": 0, "balance": 0.00003628},
{ "address": "QgHsU3UbVH2HWd3cZKsivtCTMcZjsyEYjc", "assetId": 0, "balance": -0.00010321},
{ "address": "QgJdTosTZQPzBYiWQSeCDw5zGWxa96zfkA", "assetId": 0, "balance": 0.00003628},
{ "address": "QgkGF35JZnfzzzZ3GcrLjdiE7DWGzsoLGz", "assetId": 0, "balance": -0.00010321},
{ "address": "QgmEtScSZWJmTUAidCZKj6gDr3LznZ6rr4", "assetId": 0, "balance": 0.00003628},
{ "address": "QgmJAg1X3MaQ1kp8ABKe7j6okY3RdumNfE", "assetId": 0, "balance": -0.00003608},
{ "address": "QgNtUZEQAbDAn2zbBVu8KLexZHLvDN3Rcw", "assetId": 0, "balance": 0.00003628},
{ "address": "QgnZT74VfseKiSPZgzMBVE7JpRs3N2dMs2", "assetId": 0, "balance": 0.00003628},
{ "address": "Qgp16aMcdiS2EUkxCm5NSZgB8DixGK51zT", "assetId": 0, "balance": 0.00003628},
{ "address": "QgpXUK9QEyJgFedP68iSPqD91CwoRnpB6X", "assetId": 0, "balance": 0.00003628},
{ "address": "QgqM5bKs3tNqKNAnVeaQp4oaYMXCmX6YJr", "assetId": 0, "balance": 0.00000011},
{ "address": "QgRfZM6pz7JoX8N3YheCqZCLkZbLZmAzKQ", "assetId": 0, "balance": 0.00003628},
{ "address": "QgSCgLQWuMCd9867ygUToidDaHstCaaK7X", "assetId": 0, "balance": 0.00003628},
{ "address": "QgsifXqfJtdsNbxqGYh3hExEpWWZDg9rKK", "assetId": 0, "balance": -0.00010321},
{ "address": "QgsxkxTBhBwtccex56cwbaYydp3imnikJe", "assetId": 0, "balance": 0.00003628},
{ "address": "QguDWvRKfdRv1bHDV5wqqnY1drJQTn6365", "assetId": 0, "balance": 0.00003628},
{ "address": "QgUhCiEHoA8ERQFzog8ubuCd321f1VYbDP", "assetId": 0, "balance": -0.00010321},
{ "address": "QgVRwXN3x9suBrh7Dc1HPnRiMeuLqZ5FJk", "assetId": 0, "balance": 0.00003628},
{ "address": "QgVZb632eqF1eLQm9gBGuBtyp9Dyz2FKUK", "assetId": 0, "balance": 0.00000011},
{ "address": "QgYQpgDWSMi6Rma7VqzYsuG7TWq1ChSxEv", "assetId": 0, "balance": -0.00010321},
{ "address": "QgZAyh4znJgzsb5tKGsYXXKhaZ2zYitqVg", "assetId": 0, "balance": 0.00003628},
{ "address": "Qh2cPc2Bn8fceg3wCCvkSk38oEkQ6KxWaG", "assetId": 0, "balance": -0.00010321},
{ "address": "Qh4EmrLoRwePL6mi9XZ85s1d2pkkfzj3RV", "assetId": 0, "balance": -0.00010321},
{ "address": "Qh55kmNyvRAmA7KZPAiZ5gmJSLGNNAxL5m", "assetId": 0, "balance": -0.00010321},
{ "address": "QhCcvRt4jmyFtjeqeHGeU4Z1DKdRFGmxs3", "assetId": 0, "balance": 0.00003628},
{ "address": "QhCyt8DqiumxXXJka9ErkieUWGW5AA8SvD", "assetId": 0, "balance": -0.00010321},
{ "address": "QhD2RCxdxXRKku893rvdtJbnv1bt2QR5TD", "assetId": 0, "balance": -0.00010321},
{ "address": "QhdoF3Kt3dV5DkuPgTmvH3RNzgZrSK9o6W", "assetId": 0, "balance": 0.00002352},
{ "address": "QhDPwTaDTzjSCwYHZvphyqRVhHYZ9CWmzz", "assetId": 0, "balance": 0.00003628},
{ "address": "QheP4ZKWsYzu14fewMdUayA4rkXpuH8a4p", "assetId": 0, "balance": 0.00003628},
{ "address": "QhErhzUapqPRDdXYyaS9nG8cvbCibhRUpq", "assetId": 0, "balance": -0.00010321},
{ "address": "QhH8txpLcffTmttkYjS9AWqi2Vwz6hHCBp", "assetId": 0, "balance": 0.00003628},
{ "address": "QhjbKpRtrTHegRr28KHVgP6XAZyJyjkapw", "assetId": 0, "balance": 0.00003628},
{ "address": "QhkyUQcRTxSDzTzUGJoPGnLC9N1ZiDcwnt", "assetId": 0, "balance": 0.00003628},
{ "address": "QhM6LS7TCiAiWMbvXWMWSDNJVEwHPdLb94", "assetId": 0, "balance": 0.00003628},
{ "address": "QhPwExMZk8mW4FvH2HtQGbq5mU2sTHNS9B", "assetId": 0, "balance": 0.00003628},
{ "address": "QhPXuFFRa9s91Q2qYKpSN5LVCUTqYkgRLz", "assetId": 0, "balance": 0.00003628},
{ "address": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "assetId": 0, "balance": 0.00003628},
{ "address": "QhqoEihESYJnVDTBWpEPsXub6c7eCJgAma", "assetId": 0, "balance": 0.00003628},
{ "address": "QhQsFX4iYf9f5zQp5CLQPQVzSEX2fTcSbx", "assetId": 0, "balance": -0.00010321},
{ "address": "QhsactZ9HZTkUSff3fWpRNxSZjueunYiF1", "assetId": 0, "balance": 0.00003628},
{ "address": "QhspjBT3mpnao5EeLqEY3HJFXv42uPpCks", "assetId": 0, "balance": 0.00003628},
{ "address": "QhTHNmR4YgYJu5o2uzGkgUHFpg1pYu9KU3", "assetId": 0, "balance": 0.00003628},
{ "address": "Qhuos9t2XkBCmiFiroQFwQ7CaULAZ9YBnj", "assetId": 0, "balance": 0.00003628},
{ "address": "QhwZ6thwxwaucJfbNxB2LoA17GZqfaA1D7", "assetId": 0, "balance": -0.00010321},
{ "address": "QhYD58sT9N8b6jhNgcmVnLXRuVCnbnuxUd", "assetId": 0, "balance": 0.00001276},
{ "address": "QhYS1Ag1RjYVUSGYYKyvQXSRWN9nyFGnnS", "assetId": 0, "balance": 0.00003628},
{ "address": "QhZJFejortmvM99apbw83n9RVFhFpNUCLF", "assetId": 0, "balance": 0.00003628},
{ "address": "QhzyudB9g5TieSbCi2SBtm9sS8ia2hq4oe", "assetId": 0, "balance": 0.00003628},
{ "address": "Qi1W3hiPZWH6wfGt2imicU7upZcqHy7RBv", "assetId": 0, "balance": 0.00001276},
{ "address": "Qi2Cw4zZFvHLQnZVhwjM1ygqbn6nDEB4ZN", "assetId": 0, "balance": 0.00003628},
{ "address": "Qi3N6fNRrs15EHmkxYyWHyh4z3Dp2rVU2i", "assetId": 0, "balance": 0.00000096},
{ "address": "Qi73xBLZg3PMJELtLd9GebkDCW7Kjk1juc", "assetId": 0, "balance": 0.00002352},
{ "address": "Qi8EEW1qUuG63yRShzKBh1Wb7r88UeCNZZ", "assetId": 0, "balance": -0.00001256},
{ "address": "Qi8j8NLi2wBg7JUAb9qwctXwbyLbmbN6pp", "assetId": 0, "balance": 0.00003628},
{ "address": "QiA5SLN3NASu4EStTmCAzvR4ih1Z1TwhwC", "assetId": 0, "balance": -0.00010321},
{ "address": "QibuD4c6gvXgS4iut7q3sXuVb23rgFJq2M", "assetId": 0, "balance": 0.00003628},
{ "address": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "assetId": 0, "balance": 0.00000015},
{ "address": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "assetId": 2, "balance": 0.00000004},
{ "address": "QicRwDhfk8M2CGNvpMEmYzQEjESvF7WrFY", "assetId": 0, "balance": 0.00000040},
{ "address": "QiDji6mSFEF3PjGnKfZvMwJrR1GQtnf6Pd", "assetId": 0, "balance": 0.00003628},
{ "address": "QiERDpXv985tgbL39GsKrrkbrfmKBj6bpN", "assetId": 0, "balance": 0.00003628},
{ "address": "QiFUUj4GvfHTTuAhFseuoWZm3wYemqxSDn", "assetId": 0, "balance": 0.00003628},
{ "address": "QiGkwhUZJRsg1AzcQofw78KVmbGeoobTyf", "assetId": 0, "balance": 0.00003628},
{ "address": "QiGN3Kce81GdoiWkztj58hypZ1qBUiMPnZ", "assetId": 0, "balance": 0.00003628},
{ "address": "QiGumRw6CnrTkWKkAp7pXYSBvtNsDPaoGH", "assetId": 0, "balance": 0.00003628},
{ "address": "QihzNPXWQC5HqjmzqT91GzhNpVXmveGJq6", "assetId": 0, "balance": -0.00010321},
{ "address": "QiJQdet8ziyDeCijhJXFE7MnWbX5XQpn2T", "assetId": 0, "balance": 0.00003628},
{ "address": "QiQUssukhoo1ft4G9Mxa8JpViqFW4PdBjJ", "assetId": 0, "balance": 0.00000011},
{ "address": "QiRDWHPRQcp6jQrtjqNYRDtkvGnsjryXF5", "assetId": 0, "balance": 0.00003628},
{ "address": "Qis1kSR77JUx957Lw4oV1kZsHAtZ6bL52W", "assetId": 0, "balance": 0.00003628},
{ "address": "QiSRUnc4FbFX4Mb4b8i2Aa9ebXb6r3qhNr", "assetId": 0, "balance": 0.00002352},
{ "address": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "assetId": 0, "balance": 0.00003628},
{ "address": "Qit8cfptq7zJXj9xiZRGoT8Lz36TeLjcsS", "assetId": 0, "balance": -0.00006713},
{ "address": "QiYuVgjzYfEwobgrTFVsSa2R3ut4tL2rm1", "assetId": 0, "balance": -0.00010321},
{ "address": "Qj2USguA2xGYbUFHwU9jzJwmyGCiTaQEcS", "assetId": 0, "balance": 0.00001276},
{ "address": "Qj6NdK4qoLrsHkWoDNhasSkrLLsudFMDWp", "assetId": 0, "balance": -0.00010321},
{ "address": "Qj6nk7RCJcB6gB5SZYGnKrqk6umyVD2XWT", "assetId": 0, "balance": 0.00000028},
{ "address": "QjBMFFPhQryK31Uk7jzhLaC4grbq4Lv3XM", "assetId": 0, "balance": 0.00003628},
{ "address": "QjdMUSewptx5M9KXUrx8HPSVZPXqa8JDVC", "assetId": 0, "balance": -0.00010321},
{ "address": "QjEaMxcBKMsj91ytKe6GdTBJP8Mu1Ru3r4", "assetId": 0, "balance": 0.00003628},
{ "address": "QjEAs2or122weKppv5zALzoQzXxbsDjy3f", "assetId": 0, "balance": 0.00003628},
{ "address": "Qjf1sJh7Y7e16QWjExgzQt7o2Mqxrgw977", "assetId": 0, "balance": -0.00010321},
{ "address": "Qjfqk23ZTP9wNLTPLyQzewhxgmjxh8cwv7", "assetId": 0, "balance": 0.00003628},
{ "address": "QjgGeEkyiXa43pyqkXxZbvAChQpVYfUyKz", "assetId": 0, "balance": 0.00003628},
{ "address": "QjKRbeTYYi53Pu4Ph3t8seavsoay3N8zpi", "assetId": 0, "balance": 0.00003628},
{ "address": "QjKzZNNuBYWvZkGP1YtTnbPXY12DPmhWcP", "assetId": 0, "balance": -0.00010321},
{ "address": "QjLMLprtMqwfKEN3XoocfDRVSWkbKQwm7d", "assetId": 0, "balance": 0.00000011},
{ "address": "QjMWr9osCo2eJVZyzRn5zNURn6azCC4Agx", "assetId": 0, "balance": 0.00003628},
{ "address": "QjQosFc13zX2kN52Miyo3DuBYU288jNkdW", "assetId": 0, "balance": 0.00003628},
{ "address": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "assetId": 0, "balance": 0.00000011},
{ "address": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "assetId": 2, "balance": 0.00000011},
{ "address": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "assetId": 0, "balance": 0.00003628},
{ "address": "QjTRoy39Bfq1DJD6UPiHvcCVgrA663WkSf", "assetId": 0, "balance": 0.00003628},
{ "address": "QjVm1ZaT62korzr9XvRxmJppyFF4sdafeT", "assetId": 0, "balance": 0.00003628},
{ "address": "QLcnnJygHWRvCYyxk1EUwMMWpJicgp8WkF", "assetId": 0, "balance": 0.00003628},
{ "address": "QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs", "assetId": 0, "balance": 0.00003628},
{ "address": "QLfUgN4QRkHacD6PdxfUUQUBG7NXkqz2Pg", "assetId": 0, "balance": 0.00003628},
{ "address": "QLhxpFjnYi8HToiHep6X3okP1U45bpz54S", "assetId": 0, "balance": 0.00003628},
{ "address": "QLi8RY1wquju2jXpEgJ1f9e2i1NyBQhxJy", "assetId": 0, "balance": 0.00001276},
{ "address": "QLj3L7YAX1TqCBvkMcXJ7pKoVyVwhjhhMA", "assetId": 0, "balance": 0.00002352},
{ "address": "QLjE5xQbBcALTSpnuu3Ey5SG7jqj4ke8hZ", "assetId": 0, "balance": 0.00003628},
{ "address": "QLk4souHeUSaT5jKezcmKtjUexZKyqXjqb", "assetId": 0, "balance": 0.00003628},
{ "address": "QLqSs841jDXCJQ1RJ4xq68V5V2FQtP9GkU", "assetId": 0, "balance": -0.00010321},
{ "address": "QLr2d2rVviocEfqta6cRb9uKZfH8YEGb2P", "assetId": 0, "balance": -0.00010321},
{ "address": "QLu1ZhFYAHdq5YPemwBBy6wNtJS9ZnsiV4", "assetId": 0, "balance": 0.00003628},
{ "address": "QLwMaXmDDUvh7aN5MdpY28rqTKE8U1Cepc", "assetId": 0, "balance": 0.00000103},
{ "address": "QLxSSDU5QNfpSzWpBuLSauuj6uEecqUcD1", "assetId": 0, "balance": 0.00003628},
{ "address": "QM1jwcy9hgFmjkbNHcJTv9ksS6q3gpeHNd", "assetId": 0, "balance": -0.00010321},
{ "address": "QM7gZsy5p5qxuyikPEqMUcUArDHtJ2A1Kv", "assetId": 0, "balance": -0.00010321},
{ "address": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "assetId": 0, "balance": 0.00000036},
{ "address": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "assetId": 2, "balance": 0.00000022},
{ "address": "QMC3FQSa83vDUH3ApyoVnJe6P8JTdedCiR", "assetId": 0, "balance": -0.00010321},
{ "address": "QMEFWMLuPfmFzzGH5WCECz4VE6HjenLic9", "assetId": 0, "balance": 0.00003628},
{ "address": "QMFG3ucD5qJXShA9uzD3gVnhfKfTnNnJpX", "assetId": 0, "balance": 0.00003628},
{ "address": "QMfWg9oJg49izXMeRWrsErgNnBD6mJcKiX", "assetId": 0, "balance": 0.00003628},
{ "address": "QMGgED2eawpZZNoRTGghwkMP9NLUoCYoVw", "assetId": 0, "balance": 0.00003628},
{ "address": "QMH8qGMEHvw9YPUCE2fX9recgGfSWh6Aao", "assetId": 0, "balance": -0.00010321},
{ "address": "QMHbcfCReXRZPd1xaRaFMdFdKXXUsLC3nc", "assetId": 0, "balance": 0.00003628},
{ "address": "QMiGaZbcWXdj61Rn1UGVAPgg8s31puuX1v", "assetId": 0, "balance": 0.00003628},
{ "address": "QMJwdufHY9dMoARHCUyGbMPAqUB4BcqGKm", "assetId": 0, "balance": 0.00003628},
{ "address": "QMMh94Pfs5LVE4xJee1yggViqP1YDdQHT4", "assetId": 0, "balance": 0.00000011},
{ "address": "QMNfNWsJuFwiSufnVwWpGU7bqcNgdTKF7o", "assetId": 0, "balance": 0.00003628},
{ "address": "QMozpRT9aUunfmPh7EtQ6LPoth2JFJWBXC", "assetId": 0, "balance": 0.00003628},
{ "address": "QMpb5Gxr9PTReeCN6r3BZgPMXozMpmmaQM", "assetId": 0, "balance": 0.00003628},
{ "address": "QMPNM6p7zADqo2hp4DTXdPEZLgKTQ7qJJr", "assetId": 0, "balance": -0.00010321},
{ "address": "QMQKzygMix6WVy2J1kdepSSHjJnk2nK6MK", "assetId": 0, "balance": 0.00003628},
{ "address": "QMSfC5v8AF5SntsKuKnsgLt2EbaMNWvNhz", "assetId": 0, "balance": -0.00001256},
{ "address": "QMsKXQAYKmR7dBH4P3kMLiKzYatK3h1CeS", "assetId": 0, "balance": 0.00000011},
{ "address": "QMt6UeGcA8BLkLkfAjy8AShnBtu8ECgooc", "assetId": 0, "balance": -0.00010321},
{ "address": "QMtm8wVPHGE3qHg2hMaj6SZ78D5eXw3VWZ", "assetId": 0, "balance": 0.00003628},
{ "address": "QMuWNAJ2tbeViHtBUN3yD2KARrrzcanLAd", "assetId": 0, "balance": 0.00003628},
{ "address": "QMyV3ZofyJrRTmZfpoHrPoNZo7oA9vnoZY", "assetId": 0, "balance": -0.00010321},
{ "address": "QMz6NGa9TZzChT1sMRXjiM7uR5q6Nn34Fb", "assetId": 0, "balance": 0.00003628},
{ "address": "QMzbEtBrrjBDoFFz6n7bd6uzLWy3iXeZhN", "assetId": 0, "balance": 0.00003628},
{ "address": "QMZJe6ZxJ7rVQmX2nUqH6JdUAXXyJTvJHu", "assetId": 0, "balance": 0.00003628},
{ "address": "QN289qYSyeBD5jS3vKnqHZ4nNnpqbeh2n6", "assetId": 0, "balance": 0.00001276},
{ "address": "QN8RijNFo7SDDKYgF5yiWuh86UhWpcpdGL", "assetId": 0, "balance": 0.00003628},
{ "address": "QN95wtwKG2yT7NZkpU1q1QmFpNSYcdQVZL", "assetId": 0, "balance": 0.00003628},
{ "address": "QN9E7MDY914bqaw13TxLrs47iPzW9rJF8e", "assetId": 0, "balance": -0.00010321},
{ "address": "QNAVKy8r1WXTSisFcdPjwwYHsbMW479MwX", "assetId": 0, "balance": 0.00003628},
{ "address": "QNb5Za9VKLMni3MzyzuxxjSAykXbPGSVU1", "assetId": 0, "balance": 0.00003628},
{ "address": "QNCHqRw177Ct9ExD7FiAJaN6w6yhqkNuiY", "assetId": 0, "balance": 0.00003628},
{ "address": "QNEF6iVnzXPAqhJf4x46DhXSnRrPjgqWiC", "assetId": 0, "balance": 0.00003628},
{ "address": "QNfi5TZ8LYdK1acZz2VKnChvhY5t7QRWe1", "assetId": 0, "balance": 0.00003628},
{ "address": "QNfmBzXcb3gLXmBteM4oToqakRrCFXjVuS", "assetId": 0, "balance": 0.00003628},
{ "address": "QNFsnWD53JWrQJzVAj4JWr75qS3Pu4MMXk", "assetId": 0, "balance": -0.00010321},
{ "address": "QNHBFkVUopmpYyguufnSe6DUcbThtEUu47", "assetId": 0, "balance": -0.00010321},
{ "address": "QNHdGeFJmPcDdN8prPzPL4bk2dpnJ2ZZFr", "assetId": 0, "balance": 0.00003628},
{ "address": "QNiTnonHpXTeUrgNdyYWVDPP4ZdjkLpW72", "assetId": 0, "balance": 0.00003628},
{ "address": "QNiuxkAEGm1a5K3EzPhEvnFHVCQQWuoEoh", "assetId": 0, "balance": -0.00010321},
{ "address": "QNMDKE7XTujNQkuQorcHXw6hL7qRvyaTjr", "assetId": 0, "balance": 0.00000011},
{ "address": "QNMZnDAbYDzKLYRVNpJFcEnr3411rQBguw", "assetId": 0, "balance": -0.00010321},
{ "address": "QNoRAk6XmihoFnqP6SCEFNhR66n3McCaTF", "assetId": 0, "balance": -0.00006713},
{ "address": "QNoxXt2xDKrM51adtcFLcW92wk615qA64H", "assetId": 0, "balance": 0.00003628},
{ "address": "QNqBFbZXg5STiW6F4Lj1a1v8dMaFiACpJR", "assetId": 0, "balance": 0.00003628},
{ "address": "QNrYqn8pMjx8ax7jBeQa6onzrjEEapCABf", "assetId": 0, "balance": 0.00003628},
{ "address": "QNsiHhrAQUDk5h3ecLw8bAiF2179aggSsK", "assetId": 0, "balance": 0.00003628},
{ "address": "QNuSfDpB84q9Xydrpk7Rhu5mNY5BfWSVcc", "assetId": 0, "balance": -0.00010321},
{ "address": "QNVKrjEq5bZdiDtgo64m5kz87rTHqCwvCP", "assetId": 0, "balance": 0.00000011},
{ "address": "QNw21XRyVhudVTc15XcZZ7giKGWVAndSig", "assetId": 0, "balance": 0.00003628},
{ "address": "QNw9xAm9TUerin9QsapCPL9mV6zmoXyJrh", "assetId": 0, "balance": 0.00003628},
{ "address": "QNwVgYAQZxc6KD9FMUT2dmQBLaXdnxv7yF", "assetId": 0, "balance": 0.00003628},
{ "address": "QNXCHtt6hwppn3DjKVHEn2ybmPehgNGuV8", "assetId": 0, "balance": 0.00003628},
{ "address": "QNzMU13YxVuueojNCXegU3cDXUfju7TLkB", "assetId": 0, "balance": -0.00010321},
{ "address": "QP2hds2BNPhsK7fyMHgGApzQDuGbjhEwbD", "assetId": 0, "balance": 0.00003628},
{ "address": "QP3J3GHgjqP69neTAprpYe4co33eKQiQpS", "assetId": 0, "balance": 0.00000099},
{ "address": "QP5fR7t4SJE6F5U2q2gPLEh9U63GCEr4pB", "assetId": 0, "balance": 0.00003628},
{ "address": "QP7yGYN9fJufzuFWRSjwUXhdnVokypQ3Es", "assetId": 0, "balance": -0.00010321},
{ "address": "QP8rHEbqCr8D1aUHEn3rKa2Jgtahcjf6We", "assetId": 0, "balance": -0.00006713},
{ "address": "QP8xG56L8b28h1mguSk9LuzNhxbHgAoL9b", "assetId": 0, "balance": 0.00000011},
{ "address": "QP91M2haBGwDcayzvGp7wBBM1pugC5Sse1", "assetId": 0, "balance": -0.00010321},
{ "address": "QP9vU5yTsBjuTSFxH5Cb9VXYNRHKhMNAJ4", "assetId": 0, "balance": 0.00003628},
{ "address": "QP9y1CmZRyw3oKNSTTm7Q74FxWp2mEHc26", "assetId": 0, "balance": 0.00003628},
{ "address": "QPcTWoAhYWmwjmWbQAS8muisrQVaLJMbg7", "assetId": 0, "balance": 0.00000011},
{ "address": "QPcTWoAhYWmwjmWbQAS8muisrQVaLJMbg7", "assetId": 2, "balance": 0.00000011},
{ "address": "QPEbvVBWDG7qgy4smY8nWiie78Vec8qiT9", "assetId": 0, "balance": 0.00000011},
{ "address": "QPEd3HwgZ9w6W9eYnEMuS4NmjD8iR3DMQM", "assetId": 0, "balance": 0.00003628},
{ "address": "QPfP9syFgebRP5A1s2DK7kC1L6hWFoLjoB", "assetId": 0, "balance": -0.00010321},
{ "address": "QPGvKDAhG86Z9UDyo6pSvDLUQkSCi44JfT", "assetId": 0, "balance": 0.00003628},
{ "address": "QPLoqpwAoytvpQKwvJ6GRsaRcVZ3xnYgVB", "assetId": 0, "balance": 0.00003628},
{ "address": "QPmNcXdK2EVmxZ2KSeGS5N6s8Koikwnutc", "assetId": 0, "balance": 0.00003628},
{ "address": "QPnfdCQNDDP4LTpUQPEiydgmp734mXNvb5", "assetId": 0, "balance": -0.00010321},
{ "address": "QPNuXTLsQaBzUTENu4mmhLyqTAKAVfdVym", "assetId": 0, "balance": 0.00003628},
{ "address": "QPpr2JS24d9maQtXLpNQuLqivWe17VNth8", "assetId": 0, "balance": 0.00003628},
{ "address": "QPQaXAcVz6jtP9X5oCwUhHrC6PY7jpof7r", "assetId": 0, "balance": 0.00003628},
{ "address": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "assetId": 0, "balance": 0.00003628},
{ "address": "QPqXoYhTPiDdSuwcAj9JvrnBuzTYDBJEmv", "assetId": 0, "balance": -0.00010321},
{ "address": "QPREQjU2defiYdgA33HDiLNGBpxtuebeqE", "assetId": 0, "balance": 0.00000011},
{ "address": "QPrh9z8gNmRe5SU2zmBHSbZzXawkHDiDwy", "assetId": 0, "balance": 0.00003628},
{ "address": "QPRJwmpAh2A9ed1V4ib2GYat4XEZTebPqr", "assetId": 0, "balance": -0.00010321},
{ "address": "QPsjHoKhugEADrtSQP5xjFgsaQPn9WmE3Y", "assetId": 0, "balance": 0.00000082},
{ "address": "QPUMyJ59kkrp75tDzDPxSyw1GWCrbC2cS2", "assetId": 0, "balance": 0.00000004},
{ "address": "QPusqAVBVFGAAeE7RdospttA18AuyLP7sB", "assetId": 0, "balance": 0.00003628},
{ "address": "QPV6pAxUghP23w3KDEv3PDcD9EvAypdvbJ", "assetId": 0, "balance": -0.00010321},
{ "address": "QPVCG6EUkxcuznnDRf4aDLUNSUTnWiKeA7", "assetId": 0, "balance": 0.00003628},
{ "address": "QPwESkkM7hCQ8gSa3cgY3sXB27cQMMrkU9", "assetId": 0, "balance": -0.00010321},
{ "address": "QPWxc25kgMu2ZsFnZwGz8yXdSbNnmgig6s", "assetId": 0, "balance": 0.00003628},
{ "address": "QPWzseBZj9UDGTASKeub5QTGwhpvTvGrAf", "assetId": 0, "balance": 0.00003628},
{ "address": "QPYBoSu8KdPoNGkpjZo7FNy6br2Etzx7q9", "assetId": 0, "balance": 0.00003628},
{ "address": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "assetId": 0, "balance": 0.00000022},
{ "address": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "assetId": 2, "balance": 0.00000011},
{ "address": "QPyx2bNiAnJEjitfeAh8jZXzQVKio2B7Mi", "assetId": 0, "balance": 0.00003628},
{ "address": "QPZJVL5PD7ZDEWa2TfN6nx8h55MraPk6SR", "assetId": 0, "balance": 0.00003628},
{ "address": "QQ5qnof5pUgJem8NPsAPgYdENL88cNqSj9", "assetId": 0, "balance": 0.00003628},
{ "address": "QQ6FA4TgpqPc8kN4Sp9LVUZ7Wcix4kT3rc", "assetId": 0, "balance": 0.00003628},
{ "address": "QQ9VZH256J59hAQaHvbqB2DJDPfkvo8R2U", "assetId": 0, "balance": -0.00010321},
{ "address": "QQa3MTgdnru5B7wSqPcq7qXcZcpbDQ7oyE", "assetId": 0, "balance": 0.00000004},
{ "address": "QQaKBSjAt9RK2bqJoSriR77X4ULstGzrFQ", "assetId": 0, "balance": 0.00003628},
{ "address": "QQAuaqYCU2XfTuCkNn4KPbNA7txNN2om62", "assetId": 0, "balance": 0.00003628},
{ "address": "QQbzLNiPHMqtrjGYuHXNgED4F6Pc89t7am", "assetId": 0, "balance": 0.00002352},
{ "address": "QQEWYGZBbmdLL4HrQrAtnyCdzsm67GxAhr", "assetId": 0, "balance": 0.00000011},
{ "address": "QQEZEGWt3sAPwEWYD2RQ6tMwnpkayG81dY", "assetId": 0, "balance": 0.00000011},
{ "address": "QQFabMW4DtU23uUhZRe47Q4F4h2uTHvgcq", "assetId": 0, "balance": -0.00010321},
{ "address": "QQfd4mHdR3YvUXgtq1t6s5RxbnVdagqLiY", "assetId": 0, "balance": 0.00003628},
{ "address": "QQfwxmBGXXU6U88DeYqpp9k3j99g5deGD4", "assetId": 0, "balance": 0.00003628},
{ "address": "QQjiCpwLkxEdvYa5EQvrKoxAL6dA6uJCq7", "assetId": 0, "balance": 0.00003628},
{ "address": "QQoENGwx2bpj24aF9cuGUFd7GVWPH8Led3", "assetId": 0, "balance": 0.00003628},
{ "address": "QQPYyoE3Bm2vh8Wr5aaBNyirC8dd3BhBGH", "assetId": 0, "balance": 0.00000011},
{ "address": "QQrnqFh6AedkwRSAEzWWJUfLVtJPbfNurK", "assetId": 0, "balance": 0.00003628},
{ "address": "QQRwKAbAtVFVbydiwAFmoUivVMPxrND78o", "assetId": 0, "balance": 0.00003628},
{ "address": "QQuhcRELLCkgcc8UTGXKLQfMGY5RWMKwf3", "assetId": 0, "balance": 0.00003628},
{ "address": "QQXgH4CnQCB76BbXhsApu6ShhohFfvoXv7", "assetId": 0, "balance": 0.00003628},
{ "address": "QQYp1TiGWbRChHY8fWzeNSYrBSbyczwkcK", "assetId": 0, "balance": 0.00003628},
{ "address": "QQzMut6erjgSKCpZ1dHDcjKcj9KAce7cug", "assetId": 0, "balance": 0.00003628},
{ "address": "QR5xsQro2R42oU1bcXoZoqxqBsaKvoZkPG", "assetId": 0, "balance": 0.00003628},
{ "address": "QR9UR5QUE7yAwPyios25WQdworma6k8iLf", "assetId": 0, "balance": -0.00006713},
{ "address": "QRaDef6H2zYfefqLwYGmUg7T6DAqo6DDqc", "assetId": 0, "balance": 0.00003628},
{ "address": "QRCkZ5zUcgo7mMthsYznkbjxRgeqyKDKtD", "assetId": 0, "balance": -0.00010321},
{ "address": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "assetId": 0, "balance": 0.00000011},
{ "address": "QRctujZqsh51nbvfxmJzXcCoJDA5hRxdmp", "assetId": 0, "balance": 0.00003628},
{ "address": "QRcvSzSNezuGkLqAoeAHqVvQDGUP6CTKeq", "assetId": 0, "balance": 0.00003628},
{ "address": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "assetId": 0, "balance": 0.00003643},
{ "address": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "assetId": 2, "balance": 0.00000015},
{ "address": "QRFHr4jnVgvAsPTubeSrh8bPy1yzwzYaWD", "assetId": 0, "balance": 0.00000011},
{ "address": "QRHCneJApSW2qe2uuo1QkFq5Xb4qx5YfpK", "assetId": 0, "balance": -0.00010321},
{ "address": "QRHj3FnBzzDM314JVi4HNcvAHit5EXamLo", "assetId": 0, "balance": 0.00002352},
{ "address": "QRiAaFKLPgScKPUjGHEA5uTa1gjt8ZRXSv", "assetId": 0, "balance": -0.00010321},
{ "address": "QRJRPTC431ortbmXizawh3JM64Vd1qGWhu", "assetId": 0, "balance": 0.00003628},
{ "address": "QRk5TG57SQGLkybXUqxBnobADTFGj9GR3Z", "assetId": 0, "balance": 0.00003628},
{ "address": "QRKRk5HVADsN1LHygK7q2pA7dWnYKnPpCT", "assetId": 0, "balance": 0.00000011},
{ "address": "QRmdkrmtJrjXyrGLGEPB981KLw5GddvXgw", "assetId": 0, "balance": 0.00001276},
{ "address": "QRMYnBhWD1ncLWiMrMeRiMpcTnc4dv3sTb", "assetId": 0, "balance": -0.00010321},
{ "address": "QRnLRt2D4hkKFsyxq2UUfUH5mGwchJc25h", "assetId": 0, "balance": 0.00003628},
{ "address": "QRP5BTeMLUqWbkES3gRqFHsWh4ey8Bot2v", "assetId": 0, "balance": 0.00003628},
{ "address": "QRQa5tidBjxWd29s8Rqvmcv7Lm1irtPnio", "assetId": 0, "balance": -0.00010321},
{ "address": "QRrvsUPv9Xv6EL9M3uEWiJiSMDV4uQc1zv", "assetId": 0, "balance": -0.00010321},
{ "address": "QRt11DVBnLaSDxr2KHvx92LdPrjhbhJtkj", "assetId": 0, "balance": 0.00000103},
{ "address": "QRtRELSSASzqiYy2FtNcrePH6TVnqJkv9B", "assetId": 0, "balance": 0.00000011},
{ "address": "QRtwMe8xmGic45KkXJ2mADFmbLq4fnnY4g", "assetId": 0, "balance": -0.00010321},
{ "address": "QRu2rd4V1wnQ8yifhk18JgCTyvpoZMTg8j", "assetId": 0, "balance": 0.00003628},
{ "address": "QRUbzEbLd7fRjAx2fBdXAH4QS1WQyetvDc", "assetId": 0, "balance": 0.00003628},
{ "address": "QRViAhwyGycgNbRZ4ywQHEWtVDcN6L1e5q", "assetId": 0, "balance": -0.00010321},
{ "address": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "assetId": 0, "balance": 0.00003628},
{ "address": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "assetId": 0, "balance": 0.00003628},
{ "address": "QRwxvvk5UBLjwYQTXxZG1Xa8yYssGKTUKj", "assetId": 0, "balance": 0.00003628},
{ "address": "QRZWZQP7Tmi4orAWcHWhXpfmjtK4TdCuSu", "assetId": 0, "balance": -0.00010321},
{ "address": "QS1i9K7iJb49TA4w43VSC3fEURF6bRXvw9", "assetId": 0, "balance": 0.00003628},
{ "address": "QS2wQpMWBXaN8tV1hvWoJVSXtFKoJ4jBDJ", "assetId": 0, "balance": -0.00010321},
{ "address": "QS3kGsoFhyPeeyCbQcGiMH3LvP2KYNaKxe", "assetId": 0, "balance": 0.00003628},
{ "address": "QS49qPLS7w4xqBdcbYqwUwKjY2wN7AVmxu", "assetId": 0, "balance": -0.00010321},
{ "address": "QS7K9EsSDeCdb7T8kzZaFNGLomNmmET2Dr", "assetId": 0, "balance": 0.00003628},
{ "address": "QSApBAn8pD6jmLs6j4WwxeCa341Crb9yp4", "assetId": 0, "balance": 0.00003628},
{ "address": "QSAtkxer4LwkdQqzStB82K74CNSXuamx8x", "assetId": 0, "balance": 0.00003628},
{ "address": "QSbHwxaBh5P7wXDurk2KCb8d1sCVN4JpMf", "assetId": 0, "balance": 0.00000103},
{ "address": "QSBVtqNoaM9pfi8zMgMY9pTQhaF6XjMUZU", "assetId": 0, "balance": -0.00010321},
{ "address": "QScBgSw74MquesXmVJxerX3YgyhtShRr4q", "assetId": 0, "balance": 0.00003628},
{ "address": "QSE7sy84kVtiB5tQiRWcSXZmQX5NG3tM1k", "assetId": 0, "balance": 0.00003628},
{ "address": "QSFhD2auWxoBqBzMZggf1FqTzoUxz7cddo", "assetId": 0, "balance": 0.00003628},
{ "address": "QSGB4Rd2xhd6UmA9LALTQ4f89Tfsz5VajU", "assetId": 0, "balance": 0.00000011},
{ "address": "QSgFEURyKLVh8z84Wxb6MJxDSvY7DaRuUM", "assetId": 0, "balance": -0.00010321},
{ "address": "QSH7dFCpRkbxvfrAeAxK81u5HyBbgbUHs9", "assetId": 0, "balance": 0.00003628},
{ "address": "QSHLf7MR3LtKN5oeWewqJPEmgMBDVRB6Pb", "assetId": 0, "balance": 0.00003628},
{ "address": "QSJmwFYNx8mGn1n791WFzJW8BqJqZZZwRt", "assetId": 0, "balance": 0.00003628},
{ "address": "QSjqYaBA8euuKZsvJQu9moQmaPkPgjnxUL", "assetId": 0, "balance": -0.00010321},
{ "address": "QSKaxQHPYatp6YcE33CmtwiovP1qZAJZSe", "assetId": 0, "balance": 0.00003628},
{ "address": "QSkicapNH35a3UebSxxSMCfntBhwwi6veW", "assetId": 0, "balance": 0.00003628},
{ "address": "QSMZpdZWbMZQa7wxcywzrzaWTQTN216mjk", "assetId": 0, "balance": 0.00003628},
{ "address": "QSq7VrrognfVmGLrPhmpRVZZmHw5LApnD5", "assetId": 0, "balance": -0.00010321},
{ "address": "QSq8y4ZrSbF55ZddWNcw1ett2LDtjQEvNn", "assetId": 0, "balance": 0.00003628},
{ "address": "QSU5p6Q78dQM44hCDCJwnWgbVyXPg9wMH4", "assetId": 0, "balance": 0.00003628},
{ "address": "QSUruudcrhmPuM9v4JAoSnAdeQpFjQUtwG", "assetId": 0, "balance": -0.00010321},
{ "address": "QSwwCXx9hJgM7ZAVvFb1oQjrMcSfgBcDqy", "assetId": 0, "balance": -0.00010321},
{ "address": "QSZSkfeNcaK2fKLJiF6TwVuZuEt4opALN4", "assetId": 0, "balance": 0.00003628},
{ "address": "QT2X4TSA8mG7UitNFwY5DkkV7WS1RRPn7R", "assetId": 0, "balance": 0.00003628},
{ "address": "QT5PXfTynkrhckdqd6L5NyGxeWVBgXtMQC", "assetId": 0, "balance": 0.00002352},
{ "address": "QT6K1KJ3ED3wm7Fdc2ETW27spHWdjYhAXG", "assetId": 0, "balance": -0.00010321},
{ "address": "QTBG5F778g7j2yw82ReDZuAqyLC3xe1RCu", "assetId": 0, "balance": 0.00003628},
{ "address": "QTbrzCB9nnAv7Vno5dEAw4NXxAfVoNwyWA", "assetId": 0, "balance": 0.00003628},
{ "address": "QTd6P8ZuoG36VRE9W3VhtvRWQgHN3qTkhT", "assetId": 0, "balance": -0.00010321},
{ "address": "QTdSGHWUaEjx1kW1AZdRAZPCkaNwcqDCPe", "assetId": 0, "balance": 0.00001276},
{ "address": "QTdWujAFt2ErKotw4cjiorqLUqCiWCMz9i", "assetId": 0, "balance": -0.00010321},
{ "address": "QTecbuir4YPxLQ9c9Ht1TVrkHTKfnAALBd", "assetId": 0, "balance": 0.00003628},
{ "address": "QTEE4ZJXv68ke4841HWjTLAAU8mfccxwbE", "assetId": 0, "balance": 0.00000004},
{ "address": "QTFg4go5uJ1oZidqRCXqu7miyUKiqzWuD2", "assetId": 0, "balance": 0.00169901},
{ "address": "QTGBUbMv9cKMxbrrCQBiXtj6XUEyYumNns", "assetId": 0, "balance": -0.00010321},
{ "address": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "assetId": 0, "balance": 0.00000011},
{ "address": "QTgtYSdWErieArhJ7eznKSv451TqCxMYxa", "assetId": 0, "balance": 0.00003628},
{ "address": "QTKKxJXRWWqNNTgaMmvw22Jb3F5ttriSah", "assetId": 0, "balance": 0.00000103},
{ "address": "QTM9jb15o2kU9fT6ARQdYJGWfH5xC1vtCt", "assetId": 0, "balance": -0.00010321},
{ "address": "QTMczbPVBQ4Yvr3GdjS6YeLjRCBn8hvx68", "assetId": 0, "balance": 0.00002352},
{ "address": "QTMTFswUU83XVmk6T4Gez7qUJCccbAad7S", "assetId": 0, "balance": 0.00000004},
{ "address": "QTpYQqRyMekaEuECziirzy3HvCVofZS1wJ", "assetId": 0, "balance": 0.00003628},
{ "address": "QTRAvkMBrHEt4sDYAa6dHUNeGjmcfAYtys", "assetId": 0, "balance": 0.00003628},
{ "address": "QTrJucEocNy7MHXvVqWWN82TGnm7ssue2H", "assetId": 0, "balance": -0.00010321},
{ "address": "QTSi7WDsgJtCmGpE9vJot32dmozM21bDrR", "assetId": 0, "balance": 0.00003628},
{ "address": "QTSrDNbWFxFUzpCX9MnoGuBKgwP1oQqjsg", "assetId": 0, "balance": -0.00010321},
{ "address": "QTsutcJRvjMV8MhTvuetGL6rPEAnvcdYZB", "assetId": 0, "balance": 0.00003628},
{ "address": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "assetId": 0, "balance": 0.00000011},
{ "address": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "assetId": 2, "balance": 0.00000011},
{ "address": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "assetId": 0, "balance": 0.00003628},
{ "address": "QTVLRwoopfg7qSG9CMfCPJz3UydnT3jDxD", "assetId": 0, "balance": 0.00000014},
{ "address": "QTW9TTM7fM4ghv1UAfa4L6w25D9PsKeh3f", "assetId": 0, "balance": 0.00001276},
{ "address": "QTWc2J5jHUEeqyxSCQvnZu1GXuEWdFN8U3", "assetId": 0, "balance": -0.00010321},
{ "address": "QTx98gU8ErigXkViWkvRH5JfaMpk8b3bHe", "assetId": 0, "balance": 0.00003628},
{ "address": "QTxK2iBYyj3Qwcfwo8vjtVvmLQmZVVME1D", "assetId": 0, "balance": 0.00003628},
{ "address": "QTy8J1dtbWc5KFBYJfLcoQAbzktV4JsxNp", "assetId": 0, "balance": 0.00003628},
{ "address": "QTyokTJrR4b2y76An3BFUEbqQy5vvg76iN", "assetId": 0, "balance": 0.00003628},
{ "address": "QTZEiy2RgGgyPpkMWE6trKYRHSGqPMufM5", "assetId": 0, "balance": 0.00003628},
{ "address": "QTZh1TbhwyYWUdsMaTLVnWikotRBDvRwVz", "assetId": 0, "balance": 0.00003628},
{ "address": "QU5sn5xsq5CwtzSQ8bqbgQkFR4CVUymtA5", "assetId": 0, "balance": 0.00003628},
{ "address": "QU8vFmk1xUoRTFQuRupck4HSeeuYFAVMjw", "assetId": 0, "balance": 0.00003628},
{ "address": "QUaH6kB6Jk5mZfsFpdyKvYzFA12j4g2Bss", "assetId": 0, "balance": -0.00010321},
{ "address": "QUarYYMPRodjEEBKrGsTsufPa1pc5M9mVk", "assetId": 0, "balance": 0.00003628},
{ "address": "QUAXAog3G9F8cJMC3KL4iGGFC3AK5hrzzP", "assetId": 0, "balance": 0.00003628},
{ "address": "QUCbBSPjDjygRJehHwjcXtM7PngUXKMiLW", "assetId": 0, "balance": 0.00003628},
{ "address": "QUCbFnjNwYfugM29oh6syCMnpr68vXuQjN", "assetId": 0, "balance": -0.00010321},
{ "address": "QUdjqijDoyc83K4WcMW1sCn7zLd2t1WTqn", "assetId": 0, "balance": 0.00003628},
{ "address": "QUGo9SErgc6ceB5aBzcSJDNqBkQ9eaCKZS", "assetId": 0, "balance": 0.00003628},
{ "address": "QUgUMBsdauY9p7ahjkEkxPnH51vqZ6fEit", "assetId": 0, "balance": -0.00010321},
{ "address": "QUhQUjdExXnbX6BYNSHNYohv8WUQgDpCYP", "assetId": 0, "balance": 0.00003628},
{ "address": "QUjcZYLfVsmJdB57w4rbvPKuSe26hoX8nA", "assetId": 0, "balance": -0.00010321},
{ "address": "QUjm9fPRs4wbvXmwUYdMDg3HdNxGuR1DBo", "assetId": 0, "balance": 0.00003628},
{ "address": "QUJyCt8ZMDauaH4avg84gCbLY5Es2KJVFM", "assetId": 0, "balance": 0.00003628},
{ "address": "QUKKwug9PNai3DBggXUXP8Ag7WmR5SVUR4", "assetId": 0, "balance": 0.00003628},
{ "address": "QUNYcKorTAjcFEFH2kLuGzTHDSXHbTm9n4", "assetId": 0, "balance": 0.00000011},
{ "address": "QUNYcKorTAjcFEFH2kLuGzTHDSXHbTm9n4", "assetId": 2, "balance": 0.00000011},
{ "address": "QUo3i1Ae9apv8muRUZuKaz7oTbRdKDWKgd", "assetId": 0, "balance": 0.00003628},
{ "address": "QUon9BuHPfvwS74tju9apvSioPGRh2R9f2", "assetId": 0, "balance": 0.00003628},
{ "address": "QUQdkr2SVCBcDTXVseG7MuZshQxSwyGZB2", "assetId": 0, "balance": 0.00003628},
{ "address": "QURLLepAaEaUQuQKT3PQ1zMMTD4w8ztuxD", "assetId": 0, "balance": 0.00003628},
{ "address": "QUt4pPZnFH3Sd1NhQNg5CEbKhGZHceTqNb", "assetId": 0, "balance": 0.00003628},
{ "address": "QUTM1cfWdFFehQx2MdNENSqZKh1aqR4Z7K", "assetId": 0, "balance": 0.00003628},
{ "address": "QUvoLFfkuVuRe1KGMLQS4nUHry6CBTuTYz", "assetId": 0, "balance": 0.00000004},
{ "address": "QUvtYEENi8wPXqCE2kereZaNxNgXrVivYr", "assetId": 0, "balance": 0.00003628},
{ "address": "QUw11tpoaCGqYvXdNoLE67vbTaRGvkAP8i", "assetId": 0, "balance": 0.00003628},
{ "address": "QUwdTXDoZ5BPMeW53e2epqV987jWej2Nk6", "assetId": 0, "balance": -0.00010321},
{ "address": "QUXga5K8nzd9EqYtvEesZWEYuA688h6D3d", "assetId": 0, "balance": 0.00003628},
{ "address": "QUxh6PNsKhwJ12qGaM3AC1xZjwxy4hk1RG", "assetId": 0, "balance": 0.00000059},
{ "address": "QUXqBSukt3Lmp8qBdCMtaM2P4qFGTBarCw", "assetId": 0, "balance": 0.00003628},
{ "address": "QUxxuGuZX141B6ZzDds6oojPHGqEM3cPNV", "assetId": 0, "balance": 0.00000011},
{ "address": "QUYF2HhJF1tF4avi5xByPwwWhguHYXXLWL", "assetId": 0, "balance": -0.00010321},
{ "address": "QUYfELYYXqHxEELSfDixQUL6ZqxvgqCtxE", "assetId": 0, "balance": -0.00001256},
{ "address": "QUZCxNDBcv74PfrP9dXk1SbEsaQnKdb2Nd", "assetId": 0, "balance": 0.00003628},
{ "address": "QUZQPWhrxpze32vGiux6wa85kg9iwuhCDx", "assetId": 0, "balance": 0.00003628},
{ "address": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "assetId": 0, "balance": 0.00000011},
{ "address": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "assetId": 2, "balance": 0.00000011},
{ "address": "QV2HChYd7opM1r6oYaX7KA5VUoKdiUuagg", "assetId": 0, "balance": 0.00000007},
{ "address": "QV4496JU7VU9hwfZBwwprEGUv2d1RedQWz", "assetId": 0, "balance": 0.00003628},
{ "address": "QV7bS2gnJnTzL38eD2YjNvBZFwQwe4Mw5U", "assetId": 0, "balance": 0.00003628},
{ "address": "QVA3VtN9yYQDuptoTRCXoPDtvuwgW4pjH6", "assetId": 0, "balance": -0.00001256},
{ "address": "QVaeUJbQHTamCcbULtSiiMFHsM2fqQunsy", "assetId": 0, "balance": -0.00010321},
{ "address": "QVbDaDCrHEq8s5nfhHhFf3m712kAqbzFL5", "assetId": 0, "balance": -0.00010321},
{ "address": "QVbSXYN5wdKL5u5QZnJiYQgY9BeTGmfs7z", "assetId": 0, "balance": 0.00003628},
{ "address": "QVBWwms8Goc9ruUSPFtBt48b36uzQmKVdo", "assetId": 0, "balance": 0.00003628},
{ "address": "QVchuzuoTKxNi2aMhype6sS7HCRLhdDrvw", "assetId": 0, "balance": 0.00001276},
{ "address": "QVeSskDtxCQz7xj5GQcHrPgK5Kdtevjgc4", "assetId": 0, "balance": -0.00010321},
{ "address": "QVgFYrrQV9feh45kAT6DyBonxdiJxvpmzh", "assetId": 0, "balance": -0.00010321},
{ "address": "QVHWbjbpjg9zPXfyET7Sjb7JA9BBMWL9Qr", "assetId": 0, "balance": 0.00003628},
{ "address": "QVi5jjTjJNoUg9kXSKAQPzxNA3yYsKBnEE", "assetId": 0, "balance": 0.00003628},
{ "address": "QViKVZa3M3ar7RBRSBMTx8FdzLh1zxUhN8", "assetId": 0, "balance": 0.00003628},
{ "address": "QViPTQGYNRXN7SQQEoNKvFnEW56X2sBqj8", "assetId": 0, "balance": -0.00006713},
{ "address": "QVLuvt9krmxXwQPAeAhxzhuMF5i8F4aNs8", "assetId": 0, "balance": 0.00000015},
{ "address": "QVLuvt9krmxXwQPAeAhxzhuMF5i8F4aNs8", "assetId": 2, "balance": 0.00000015},
{ "address": "QVnHHnf5ZPBpbLZQabtjZBzi9TPgtqABqc", "assetId": 0, "balance": 0.00003628},
{ "address": "QVpVKaXziXmP8qawtxqaFN8mHFAqvuiWzY", "assetId": 0, "balance": 0.00003628},
{ "address": "QVrvy4ac2jBTfxyCKB7MLimqJooTDBApmS", "assetId": 0, "balance": 0.00003628},
{ "address": "QVSo56b2nsrEbWzi3FBkGQCLJNyk9b9j7a", "assetId": 0, "balance": -0.00010321},
{ "address": "QVSp1MvSTBut7shWdddfwdsWf9c7snBDwS", "assetId": 0, "balance": 0.00003628},
{ "address": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "assetId": 0, "balance": 0.00000015},
{ "address": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "assetId": 2, "balance": 0.00000015},
{ "address": "QVuksgNt3QAr7KCrkxtE5FWrczfgLKxs4H", "assetId": 0, "balance": 0.00003628},
{ "address": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "assetId": 0, "balance": 0.00000052},
{ "address": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "assetId": 2, "balance": 0.00000048},
{ "address": "QW1ip98ypmMmcSRjCRkS7Jd1SfneQbU7fq", "assetId": 0, "balance": 0.00001276},
{ "address": "QWb8NhsKVEnfM8NSMPdSWSdn1T4zkCDDFD", "assetId": 0, "balance": -0.00010321},
{ "address": "QWBFK5h61ZxGfqQpEkwwKTcLAo8t9VWe4K", "assetId": 0, "balance": 0.00003628},
{ "address": "QWBrxCkBSMNaL5ssPEawjfP9qUdurrFmP3", "assetId": 0, "balance": 0.00003628},
{ "address": "QWC7MydcEFhjENmCS2YABKY5F5BQd8XYyA", "assetId": 0, "balance": 0.00003628},
{ "address": "QWcgaTFfxt1cZL7hn9G8ayo81WT13S5ECM", "assetId": 0, "balance": 0.00003628},
{ "address": "QWcqcnuDeNpeUYqcvsJtXYdCpDb35ehAv6", "assetId": 0, "balance": -0.00010321},
{ "address": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "assetId": 0, "balance": 0.00000059},
{ "address": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "assetId": 2, "balance": 0.00000059},
{ "address": "QWFHfqYNCYW9EN63zdprYSFQx2ApS6Hj2z", "assetId": 0, "balance": 0.00003628},
{ "address": "QWigG4GAT8eQ6rmNo4AdcGjF5ygSTAV1Q1", "assetId": 0, "balance": -0.00010321},
{ "address": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "assetId": 0, "balance": 0.00003628},
{ "address": "QWK94e5PzrBN5gHrFd77dHeP5XtCiWxVj5", "assetId": 0, "balance": 0.00001276},
{ "address": "QWKYjxBUt2c6BHm26c4k7U8iF9eUEEAeQy", "assetId": 0, "balance": 0.00003628},
{ "address": "QWL7kZp6Pdd1bhxZ6SXPhVf5g7GParG9CC", "assetId": 0, "balance": 0.00003628},
{ "address": "QWLpsGYrkF2cy3tH6DCxso7kXZpZJvv13e", "assetId": 0, "balance": 0.00000011},
{ "address": "QWMEPx9QfK4ErsHx4RwyoWL1cf4xqpBzXy", "assetId": 0, "balance": -0.00010321},
{ "address": "QWmkcX9Ak4EJMZ6JZskF5uqBxiqK6R9c8s", "assetId": 0, "balance": 0.00003628},
{ "address": "QWN4qgyBfSn9TRTJM9e8ftzuAZmSuadmt5", "assetId": 0, "balance": 0.00002352},
{ "address": "QWomBbcXNTdkyuPFUafwtBfpbxHzmUZzqi", "assetId": 0, "balance": 0.00000011},
{ "address": "QWomBbcXNTdkyuPFUafwtBfpbxHzmUZzqi", "assetId": 2, "balance": 0.00000011},
{ "address": "QWRpDYNycvqQrL9RmMDraL1hjTRBbghekz", "assetId": 0, "balance": 0.00003628},
{ "address": "QWS3EtVcBFz9iFjeANx1hTNN8Pfxi2Ft6H", "assetId": 0, "balance": 0.00003628},
{ "address": "QWuW2YMygVtWieUo6a4yayD1xFDWdnmo5j", "assetId": 0, "balance": 0.00000011},
{ "address": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "assetId": 0, "balance": 0.00000011},
{ "address": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "assetId": 2, "balance": 0.00000011},
{ "address": "QWwBj6cFoM5EAE7sXULVw2BjVMCPTxDmVs", "assetId": 0, "balance": 0.00003628},
{ "address": "QWwcZuuDMpZtzFvjQthW6FUaUwpkzsmBN4", "assetId": 0, "balance": 0.00003628},
{ "address": "QWwrtjBL4ah965XPXHYJhymreC9jyryNLZ", "assetId": 0, "balance": 0.00003628},
{ "address": "QWYxUJmR6M6tvyxZASux3pxWuq1iWqTPei", "assetId": 0, "balance": -0.00010321},
{ "address": "QWzjhwJg7u7EAfJVvRffiENs5ufhevZNso", "assetId": 0, "balance": 0.00003628},
{ "address": "QX5g6nyYJrbiMxdcHdcHgbfA8jURQcEZGZ", "assetId": 0, "balance": -0.00010321},
{ "address": "QX6TiCGH3oJKucGW2vEYU3kRKBuXSZLZtn", "assetId": 0, "balance": 0.00003628},
{ "address": "QX8mxo977eANNG6Q59Z4dCW3eX3HPBrZ1R", "assetId": 0, "balance": 0.00000011},
{ "address": "QX92FzQwtqm4svY7TR4gxt1aVAjEzdNnKo", "assetId": 0, "balance": 0.00003628},
{ "address": "QXapyoyeuUZ44m8PdJ2XcMdADkrMeAeRzF", "assetId": 0, "balance": 0.00003628},
{ "address": "QXaXcaaL1eDZQaECk47BCsjHojWGfHLcw2", "assetId": 0, "balance": 0.00003628},
{ "address": "QXB9jbqCrYBA68vgzrr8Z8bqMXrCvyU7Z1", "assetId": 0, "balance": 0.00003628},
{ "address": "QXdyEzgLMniSSzf2PS7hQhqUX6XKQemJnv", "assetId": 0, "balance": 0.00001276},
{ "address": "QXeW3vzV8Rfe9kUbm15BW9dFFuXuBq8feb", "assetId": 0, "balance": 0.00003628},
{ "address": "QXFVYCWTAnM3FVhpYkin3Yu7WPb8w7NNZ2", "assetId": 0, "balance": -0.00010321},
{ "address": "QXHEZ4axuNq91K5wW9zaNSvtLzsdsQ1yVz", "assetId": 0, "balance": 0.00002352},
{ "address": "QXHMjGxDjd1RbNN4o2XdmXBdpiS6emQ8QL", "assetId": 0, "balance": 0.00003628},
{ "address": "QXHmtFXzf4D7PEu73NfBm3sZyeuGrm3QC5", "assetId": 0, "balance": 0.00003628},
{ "address": "QXjqVCQ8RaaC7T6Tyiag26Ruj1Tyrx9PvP", "assetId": 0, "balance": 0.00003628},
{ "address": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "assetId": 0, "balance": 0.00000026},
{ "address": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "assetId": 2, "balance": 0.00000015},
{ "address": "QXm5e16Lq6dnYwpZJ8Rn2cME3ziHZfRRnp", "assetId": 0, "balance": 0.00001276},
{ "address": "QXmAdL5wEpgWbTSgnHJgdfQmKkhnx4EfaC", "assetId": 0, "balance": -0.00010321},
{ "address": "QXmjUhJ7hmcQRrZ1UvnJAeFhp3aYiwL3zq", "assetId": 0, "balance": 0.00003628},
{ "address": "QXmYaDzKQdGiAMncJCr1FqXy6tX3avMRm9", "assetId": 0, "balance": 0.00003632},
{ "address": "QXmYaDzKQdGiAMncJCr1FqXy6tX3avMRm9", "assetId": 2, "balance": 0.00000004},
{ "address": "QXNzWKLR9pqHW5KCUCFvcaUTwWKWvdYhzi", "assetId": 0, "balance": -0.00010321},
{ "address": "QXqc1jH2DL6H3qFCHxTBABivtkqJsoBYyQ", "assetId": 0, "balance": 0.00003628},
{ "address": "QXrkYRBJkp3CQ2ryjvWskszuWTXRRLbhTB", "assetId": 0, "balance": -0.00010321},
{ "address": "QXUWuZ2oAUodMU8EAQkAkDwkQHS1SFxpts", "assetId": 0, "balance": 0.00000011},
{ "address": "QXUWuZ2oAUodMU8EAQkAkDwkQHS1SFxpts", "assetId": 2, "balance": 0.00000011},
{ "address": "QXV2EabmW5AqDa4usWyv13QvxtkSUF5LFs", "assetId": 0, "balance": -0.00010321},
{ "address": "QXXfBJz9UfAgTEAn3b9W9jxmJYtqar1P78", "assetId": 0, "balance": 0.00003628},
{ "address": "QXYk68x2tiUrDBv8eq6wd4KtBmLHYiC4zR", "assetId": 0, "balance": 0.00003628},
{ "address": "QXYY8BwyYn71nDg9UKKMveyTLPZWErrtaT", "assetId": 0, "balance": -0.00010321},
{ "address": "QXZ3Uqs3KcfZDFgCURso8upzmPxxHtD9rT", "assetId": 0, "balance": 0.00003628},
{ "address": "QXzpeNdKwoxycyqZ2UanqFGngCN72nYygj", "assetId": 0, "balance": 0.00003628},
{ "address": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "assetId": 0, "balance": 0.00000011},
{ "address": "QY1rJEuFJ5C6vp6QPczQbwUpg2F8KdPRoE", "assetId": 0, "balance": -0.00010321},
{ "address": "QY4xWXWbyU4t2zrcpZUTAR3kcXpcXw63Qn", "assetId": 0, "balance": 0.00003628},
{ "address": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "assetId": 0, "balance": 0.00003628},
{ "address": "QY82MasqEH6ChwXaETH4piMtE8Pk4NBWD3", "assetId": 0, "balance": 0.00000103},
{ "address": "QYA1jbLKSSY1q1Zo2VwuDe6vTJXQrr1wu1", "assetId": 0, "balance": -0.00010321},
{ "address": "QYAbYY1mVfCcXSoWPmVzUhvh7UBaK5enER", "assetId": 0, "balance": 0.00003628},
{ "address": "QYAYB8m9CfksGvEjGnj49q74bNDCGGaZqV", "assetId": 0, "balance": 0.00003628},
{ "address": "QYB9dJizBvcsmhFBgB4tzLAKsQvb5HQggo", "assetId": 0, "balance": 0.00003628},
{ "address": "QYcEBpuJ9RmdFGX6cdKAjSwnNVhwbtFLdr", "assetId": 0, "balance": 0.00003628},
{ "address": "QYD3kXchZ86vUyJBXNCVQ4LUvTAd6PUZW3", "assetId": 0, "balance": 0.00003628},
{ "address": "QYGcPZcRhGaY1MsiDr3VtwTXmB9TAbLFSn", "assetId": 0, "balance": 0.00000011},
{ "address": "QYGNMWBmqWgVtMWGHypAsKhDVQw5mrFZww", "assetId": 0, "balance": 0.00000011},
{ "address": "QYGrsQT4yhRUxiKfVgo8M5Sovfy1zcjUsr", "assetId": 0, "balance": 0.00003628},
{ "address": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "assetId": 0, "balance": 0.00000132},
{ "address": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "assetId": 2, "balance": 0.00000092},
{ "address": "QYHvrW3bwYFeMTUEYascXhXkBAzUkcGbqn", "assetId": 0, "balance": 0.00003628},
{ "address": "QYicTvqqPFt7buJfRd9cgs2xvJ2rnvyTzX", "assetId": 0, "balance": 0.00003628},
{ "address": "QYkpsUvut4eufXqJUnUbCzDajK5RyQ1Vzg", "assetId": 0, "balance": 0.00003628},
{ "address": "QYn2Uh4eii4SE29BpEPeRySbAeb9R6tGbf", "assetId": 0, "balance": 0.00003628},
{ "address": "QYoNKJhgva9ECexhgAmB3r4ucM8xwJbTWu", "assetId": 0, "balance": 0.00003628},
{ "address": "QYp5W4kGvCHfzeCgDyoCAWBZ9gViECNS5J", "assetId": 0, "balance": 0.00003628},
{ "address": "QYphDYA1te9acFNc7FEmFBu3FTTomp4ATZ", "assetId": 0, "balance": 0.00003628},
{ "address": "QYppKiFc7zt1EDXy1dUwHMHsnm2ckVsHTc", "assetId": 0, "balance": 0.00002352},
{ "address": "QYREQw3ohthywupqzLBRMjGkRSvbFPLBow", "assetId": 0, "balance": -0.00010321},
{ "address": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "assetId": 0, "balance": 0.00000011},
{ "address": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "assetId": 2, "balance": 0.00000011},
{ "address": "QYt1n7g68RrttdF8BdnqZkkKcYq1RHTeBF", "assetId": 0, "balance": 0.00003628},
{ "address": "QYTDS3XqzHWcqmhXTsDcUDVAbQVXXVaVVs", "assetId": 0, "balance": -0.00010321},
{ "address": "QYTmTSxB8GdnruZWA7Dvod9ihRQrAiLxn1", "assetId": 0, "balance": 0.00001276},
{ "address": "QYZj8LFUwQVKZGBLe9FTgWnf6pLEyJJDZi", "assetId": 0, "balance": 0.00003628},
{ "address": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "assetId": 0, "balance": 0.00003628},
{ "address": "QZ1iWoraqiezeAHrgTsC13MTcrwHJdRwgk", "assetId": 0, "balance": -0.00010321},
{ "address": "QZ2gi6BhUNpGmrErgJLFuY1WHy6xK1J7qX", "assetId": 0, "balance": 0.00003628},
{ "address": "QZ7wvWAUcHKRhvQ3ijdrqM4zucQKCgQ1hQ", "assetId": 0, "balance": 0.00003628},
{ "address": "QZb1jPdakvcB9f7aVRU3wLXRcixgk8tPdU", "assetId": 0, "balance": 0.00003628},
{ "address": "QZb81oH9N6M4ZjPstDJuceARrdjLi8dY1x", "assetId": 0, "balance": 0.00003628},
{ "address": "QZbBJuoU892QYTQ4N1sJT9bVE3HNNdSw55", "assetId": 0, "balance": -0.00003608},
{ "address": "QZBTByprtp1MGQbEND6H95cPsrGaKEJEmy", "assetId": 0, "balance": 0.00001276},
{ "address": "QZCauGWyChXBgEQiXAJLmSaaz94Asgi8wU", "assetId": 0, "balance": 0.00003628},
{ "address": "QZgpMDQeZ3ReC13wnTvP94hVJoyAgVXEs6", "assetId": 0, "balance": 0.00003628},
{ "address": "QZHC8bBNbHTSEmdjKMQJFHAJhRwrTBXje1", "assetId": 0, "balance": 0.00003628},
{ "address": "QZiMCvxMJqG3bG6SsET43zegm4mtm2TABA", "assetId": 0, "balance": 0.00003628},
{ "address": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "assetId": 0, "balance": 0.00003639},
{ "address": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "assetId": 2, "balance": 0.00000011},
{ "address": "QZJc1V32oFm8tufB4bk7fa3aepu4EdkeDU", "assetId": 0, "balance": 0.00003628},
{ "address": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "assetId": 0, "balance": 0.00001276},
{ "address": "QZKKSYCnTaB56dT1dkXiV86eU6Pc9ADos2", "assetId": 0, "balance": 0.00003628},
{ "address": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "assetId": 0, "balance": 0.00000026},
{ "address": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "assetId": 2, "balance": 0.00000015},
{ "address": "QZMhBGpATjZ9ZK3fdcRvXW3RWKAAETymQa", "assetId": 0, "balance": 0.00001276},
{ "address": "QZMzF4iTBV93LP5Vkv7Ka3Q2xjdUwcUhcV", "assetId": 0, "balance": 0.00000103},
{ "address": "QZNDNgaBJhkUjtb66hGvjAs3s1V2TESDxE", "assetId": 0, "balance": 0.00003628},
{ "address": "QZpCNquYLc5B6xiUwsPtMB7M6f1CWcLBwP", "assetId": 0, "balance": 0.00003628},
{ "address": "QZqfJg1raAA3AzuivGD6sCQfQQcekAM6tx", "assetId": 0, "balance": 0.00003628},
{ "address": "QZRBPwvFBv59rZ4MzuPnjVi7cq5Uv7WqqR", "assetId": 0, "balance": 0.00003628},
{ "address": "QZtebdFopCUyGBQs5S5WYPckPcVZh19E4q", "assetId": 0, "balance": -0.00010321},
{ "address": "QZvHW7amu5DNktsBgaMrR1brHZhhhVwKLW", "assetId": 0, "balance": 0.00000011},
{ "address": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "assetId": 0, "balance": 0.00003639},
{ "address": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "assetId": 2, "balance": 0.00000011},
{ "address": "QZWL5atv3jQi3SdcQPS91vGhbk4Mi5CF8z", "assetId": 0, "balance": 0.00001276},
{ "address": "QZxNth97o4UNw6XbDY7fnykzuKaxmmqaR1", "assetId": 0, "balance": 0.00003628},
{ "address": "QZXo75Sk5AHuuuRX4VcHCBvcHaCHGHBVa2", "assetId": 0, "balance": -0.00010321},
{ "address": "QZzxwbQZ7Gi4kSVa39bcXb3q12AhGRQDXA", "assetId": 0, "balance": -0.00010321}
]

View File

@@ -1,13 +1,13 @@
INVALID_ADDRESS = ung\u00FCltige adresse
INVALID_ADDRESS = ungültige adresse
INVALID_ASSET_ID = ung\u00FCltige asset ID
INVALID_ASSET_ID = ungültige asset ID
INVALID_DATA = ung\u00FCltige daten
INVALID_DATA = ungültige daten
INVALID_PUBLIC_KEY = ung\u00FCltiger public key
INVALID_PUBLIC_KEY = ungültiger public key
INVALID_SIGNATURE = ung\u00FCltige signatur
INVALID_SIGNATURE = ungültige signatur
JSON = JSON nachricht konnte nicht geparsed werden

View File

@@ -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
# 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
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
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
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
# 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_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_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_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_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_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_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_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_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
# 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
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
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
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
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
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
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)
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
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
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
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# Keys are from api.ApiError enum
ADDRESS_UNKNOWN = неизвестная учетная запись
BLOCKCHAIN_NEEDS_SYNC = блокчейн должен сначала синхронизироваться
# Blocks
BLOCK_UNKNOWN = неизвестный блок
CANNOT_MINT = аккаунт не может чеканить
GROUP_UNKNOWN = неизвестная группа
INVALID_ADDRESS = неизвестный адрес
# Assets
INVALID_ASSET_ID = неверный идентификатор актива
INVALID_CRITERIA = неверные критерии поиска
INVALID_DATA = неверные данные
INVALID_HEIGHT = недопустимая высота блока
INVALID_NETWORK_ADDRESS = неверный сетевой адрес
INVALID_ORDER_ID = неверный идентификатор заказа актива
INVALID_PRIVATE_KEY = неверный приватный ключ
INVALID_PUBLIC_KEY = недействительный открытый ключ
INVALID_REFERENCE = неверная ссылка
# Validation
INVALID_SIGNATURE = недействительная подпись
JSON = не удалось разобрать сообщение json
NAME_UNKNOWN = имя неизвестно
NON_PRODUCTION = этот вызов API не разрешен для производственных систем
ORDER_UNKNOWN = неизвестный идентификатор заказа актива
PUBLIC_KEY_NOT_FOUND = открытый ключ не найден
REPOSITORY_ISSUE = ошибка репозитория
TRANSACTION_INVALID = транзакция недействительна: %s (%s)
TRANSACTION_UNKNOWN = транзакция неизвестна
TRANSFORMATION_ERROR = не удалось преобразовать JSON в транзакцию
UNAUTHORIZED = вызов API не авторизован

View File

@@ -19,6 +19,8 @@ CREATING_BACKUP_OF_DB_FILES = Creating backup of database files...
DB_BACKUP = Database Backup
DB_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

View File

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

View File

@@ -1,164 +1,176 @@
ACCOUNT_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
ACCOUNT_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
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_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_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
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_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_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
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
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_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
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
BAN_EXISTS = \u00D0\u0091\u00D0\u00B0\u00D0\u00BD
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
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
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
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
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
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_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_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_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
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
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
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
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
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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
NO_BALANCE = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D1\u0081\u00D0\u00B0
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
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
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_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
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_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_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
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
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
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_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
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
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_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
ACCOUNT_ALREADY_EXISTS = аккаунт уже существует
ACCOUNT_CANNOT_REWARD_SHARE = аккаунт не может делиться вознаграждением
ALREADY_GROUP_ADMIN = уже администратор группы
ALREADY_GROUP_MEMBER = уже член группы
ALREADY_VOTED_FOR_THAT_OPTION = уже проголосовали за этот вариант
ASSET_ALREADY_EXISTS = актив уже существует
ASSET_DOES_NOT_EXIST = Актив не существует
ASSET_DOES_NOT_MATCH_AT = актив не совпадает с АТ
ASSET_NOT_SPENDABLE = актив не подлежит расходованию
AT_ALREADY_EXISTS = AT уже существует
AT_IS_FINISHED = AT в завершении
AT_UNKNOWN = не известный АТ
BANNED_FROM_GROUP = исключен из группы
BAN_EXISTS = Бан
BAN_UNKNOWN = не известный бан
BUYER_ALREADY_OWNER = покупатель уже собственник
CLOCK_NOT_SYNCED = часы не синхронизированы
DUPLICATE_OPTION = дублировать вариант
GROUP_ALREADY_EXISTS = группа уже существует
GROUP_APPROVAL_DECIDED = гуппа одобрена
GROUP_APPROVAL_NOT_REQUIRED = гупповое одобрение не требуется
GROUP_DOES_NOT_EXIST = группа не существует
GROUP_ID_MISMATCH = не соответствие идентификатора группы
GROUP_OWNER_CANNOT_LEAVE = владелец группы не может уйти
HAVE_EQUALS_WANT = иммеются равные желания
INSUFFICIENT_FEE = недостаточная плата
INVALID_ADDRESS = недействительный адрес
INVALID_AMOUNT = недопустимая сумма
INVALID_ASSET_OWNER = недействительный владелец актива
INVALID_AT_TRANSACTION = недействительная АТ транзакция
INVALID_AT_TYPE_LENGTH = недействительно для типа длины AT
INVALID_CREATION_BYTES = недопустимые байты создания
INVALID_DATA_LENGTH = недопустимая длина данных
INVALID_DESCRIPTION_LENGTH = недопустимая длина описания
INVALID_GROUP_APPROVAL_THRESHOLD = недопустимый порог утверждения группы
INVALID_GROUP_ID = недопустимый идентификатор группы
INVALID_GROUP_OWNER = недопу владелец группы
INVALID_LIFETIME = недопу срок службы
INVALID_NAME_LENGTH = недопустимая длина группы
INVALID_NAME_OWNER = недопустимое имя владельца
INVALID_OPTIONS_COUNT = неверное количество опций
INVALID_OPTION_LENGTH = недопустимая длина опции
INVALID_ORDER_CREATOR = недопустимый создатель заказа
INVALID_PAYMENTS_COUNT = неверный подсчет платежей
INVALID_PUBLIC_KEY = недействительный открытый ключ
INVALID_QUANTITY = недопустимое количество
INVALID_REFERENCE = неверная ссылка
INVALID_RETURN = недопустимый возврат
INVALID_REWARD_SHARE_PERCENT = недействительный процент награждения
INVALID_SELLER = недействительный продавец
INVALID_TAGS_LENGTH = недействительная длина тэгов
INVALID_TX_GROUP_ID = недействительный идентификатор группы передачи
INVALID_VALUE_LENGTH = недопустимое значение длины
INVITE_UNKNOWN = приглашать неизветсных
JOIN_REQUEST_EXISTS = запрос на присоединение существует
MAXIMUM_REWARD_SHARES = максимальное вознаграждение
MISSING_CREATOR = отсутствующий создатель
MULTIPLE_NAMES_FORBIDDEN = несколько имен запрещено
NAME_ALREADY_FOR_SALE = имя уже в продаже
NAME_ALREADY_REGISTERED = имя уже зарегистрировано
NAME_DOES_NOT_EXIST = имя не существует
NAME_NOT_FOR_SALE = имя не продается
NAME_NOT_LOWER_CASE = иммя не должно содержать строчный регистр
NEGATIVE_AMOUNT = недостаточная сумма
NEGATIVE_FEE = недостаточная комиссия
NEGATIVE_PRICE = недостаточная стоимость
NOT_GROUP_ADMIN = не администратор группы
NOT_GROUP_MEMBER = не член группы
NOT_MINTING_ACCOUNT = счет не чеканит
NOT_YET_RELEASED = еще не выпущено
NO_BALANCE = нет баланса
NO_BLOCKCHAIN_LOCK = блокчейн узла в настоящее время занят
NO_FLAG_PERMISSION = нет разрешения на флаг
OK = OK
ORDER_ALREADY_CLOSED = заказ закрыт
ORDER_DOES_NOT_EXIST = заказа не существует
POLL_ALREADY_EXISTS = опрос уже существует
POLL_DOES_NOT_EXIST = опроса не существует
POLL_OPTION_DOES_NOT_EXIST = вариантов ответа не существует
PUBLIC_KEY_UNKNOWN = открытый ключ неизвестен
SELF_SHARE_EXISTS = поделиться долей
TIMESTAMP_TOO_NEW = новая метка времени
TIMESTAMP_TOO_OLD = старая метка времени
TOO_MANY_UNCONFIRMED = много не подтвержденных
TRANSACTION_ALREADY_CONFIRMED = транзакция уже подтверждена
TRANSACTION_ALREADY_EXISTS = транзакция существует
TRANSACTION_UNKNOWN = неизвестная транзакция
TX_GROUP_ID_MISMATCH = не соответствие идентификатора группы c хэш транзации

View File

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

View File

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

View File

@@ -15,12 +15,18 @@ import org.qortal.test.common.Common;
import static org.junit.Assert.*;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -127,6 +133,131 @@ public class RepositoryTests extends Common {
}
}
@Test
public void testTrimDeadlock() {
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch readyLatch = new CountDownLatch(1);
CountDownLatch updateLatch = new CountDownLatch(1);
CountDownLatch syncLatch = new CountDownLatch(1);
// Open connection 1
try (final HSQLDBRepository repository1 = (HSQLDBRepository) RepositoryManager.getRepository()) {
// Read AT states trim height
int atTrimHeight = repository1.getATRepository().getAtTrimHeight();
repository1.discardChanges();
// Open connection 2
try (final HSQLDBRepository repository2 = (HSQLDBRepository) RepositoryManager.getRepository()) {
// Read online signatures trim height
int onlineSignaturesTrimHeight = repository2.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
repository2.discardChanges();
Future<Boolean> f2 = executor.submit(() -> {
Object trimHeightsLock = extractTrimHeightsLock(repository2);
System.out.println(String.format("f2: repository2's trimHeightsLock object: %s", trimHeightsLock));
// Update online signatures trim height (implicit commit)
synchronized (trimHeightsLock) {
try {
System.out.println("f2: updating online signatures trim height...");
// simulate: repository2.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(onlineSignaturesTrimHeight);
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
PreparedStatement pstmt = repository2.prepareStatement(updateSql);
pstmt.setInt(1, onlineSignaturesTrimHeight);
pstmt.executeUpdate();
// But no commit/saveChanges yet to force HSQLDB error
System.out.println("f2: readyLatch.countDown()");
readyLatch.countDown();
// wait for other thread to be ready to hit sync block
System.out.println("f2: waiting for f1 syncLatch...");
syncLatch.await();
// hang on to trimHeightsLock to force other thread to wait (if code is correct), or to fail (if code is faulty)
System.out.println("f2: updateLatch.await(<with timeout>)");
if (!updateLatch.await(500L, TimeUnit.MILLISECONDS)) { // long enough for other thread to reach synchronized block
// wait period expired suggesting no concurrent access, i.e. code is correct
System.out.println("f2: updateLatch.await() timed out");
System.out.println("f2: saveChanges()");
repository2.saveChanges();
return Boolean.TRUE;
}
System.out.println("f2: saveChanges()");
repository2.saveChanges();
// Early exit from wait period suggests concurrent access, i.e. code faulty
return Boolean.FALSE;
} catch (InterruptedException | SQLException e) {
System.out.println("f2: exception: " + e.getMessage());
return Boolean.FALSE;
}
}
});
System.out.println("waiting for f2 readyLatch...");
readyLatch.await();
System.out.println("launching f1...");
Future<Boolean> f1 = executor.submit(() -> {
Object trimHeightsLock = extractTrimHeightsLock(repository1);
System.out.println(String.format("f1: repository1's trimHeightsLock object: %s", trimHeightsLock));
System.out.println("f1: syncLatch.countDown()");
syncLatch.countDown();
// Update AT states trim height (implicit commit)
synchronized (trimHeightsLock) {
try {
System.out.println("f1: updating AT trim height...");
// simulate: repository1.getATRepository().setAtTrimHeight(atTrimHeight);
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
PreparedStatement pstmt = repository1.prepareStatement(updateSql);
pstmt.setInt(1, atTrimHeight);
pstmt.executeUpdate();
System.out.println("f1: saveChanges()");
repository1.saveChanges();
System.out.println("f1: updateLatch.countDown()");
updateLatch.countDown();
return Boolean.TRUE;
} catch (SQLException e) {
System.out.println("f1: exception: " + e.getMessage());
return Boolean.FALSE;
}
}
});
if (Boolean.TRUE != f1.get())
fail("concurrency bug - simultaneous update of DatabaseInfo table");
if (Boolean.TRUE != f2.get())
fail("concurrency bug - not synchronized on same object?");
} catch (InterruptedException e) {
fail("concurrency bug: " + e.getMessage());
} catch (ExecutionException e) {
fail("concurrency bug: " + e.getMessage());
}
} catch (DataException e) {
fail("database bug");
}
}
private static Object extractTrimHeightsLock(HSQLDBRepository repository) {
try {
Field trimHeightsLockField = repository.getClass().getDeclaredField("trimHeightsLock");
trimHeightsLockField.setAccessible(true);
return trimHeightsLockField.get(repository);
} catch (IllegalArgumentException | NoSuchFieldException | SecurityException | IllegalAccessException e) {
fail();
return null;
}
}
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
@Test
public void testBlockHeightSpeed() throws DataException, SQLException {

View File

@@ -90,11 +90,11 @@ public class BlockApiTests extends ApiCommon {
for (Integer endHeight : testValues)
for (Integer count : testValues) {
if (startHeight != null && endHeight != null && count != null) {
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockRange(startHeight, endHeight, count));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockSummaries(startHeight, endHeight, count));
continue;
}
assertNotNull(this.blocksResource.getBlockRange(startHeight, endHeight, count));
assertNotNull(this.blocksResource.getBlockSummaries(startHeight, endHeight, count));
}
}

View File

@@ -0,0 +1,426 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
public class AtRepositoryTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGetATStateAtHeightWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
Integer testHeight = 8;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetATStateAtHeightWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int maxHeight = 8;
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNull(atStateData.getStateData());
}
}
@Test
public void testGetLatestATStateWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetLatestATStatePostTrimming() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
int maxHeight = blockchainHeight + 100; // more than latest block height
Integer testHeight = blockchainHeight;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
assertEquals(testHeight, atStateData.getHeight());
// We should always have the latest AT state data available
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATData atData = repository.getATRepository().fromATAddress(atAddress);
byte[] codeHash = atData.getCodeHash();
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = null;
Long expectedValue = null;
Integer minimumFinalHeight = null;
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash,
isFinished,
dataByteOffset,
expectedValue,
minimumFinalHeight,
limit, offset, reverse);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetMatchingFinalATStatesWithDataValue() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATData atData = repository.getATRepository().fromATAddress(atAddress);
byte[] codeHash = atData.getCodeHash();
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = MachineState.HEADER_LENGTH + 0;
Long expectedValue = 0L;
Integer minimumFinalHeight = null;
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash,
isFinished,
dataByteOffset,
expectedValue,
minimumFinalHeight,
limit, offset, reverse);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetBlockATStatesAtHeightWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
doDeploy(repository, deployer, creationBytes, fundingAmount);
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
Integer testHeight = 8;
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
// getBlockATStatesAtHeight never returns actual AT state data anyway
assertNull(atStateData.getStateData());
}
}
@Test
public void testGetBlockATStatesAtHeightWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
doDeploy(repository, deployer, creationBytes, fundingAmount);
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int maxHeight = 8;
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
// getBlockATStatesAtHeight never returns actual AT state data anyway
assertNull(atStateData.getStateData());
}
}
@Test
public void testSaveATStateWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight - 2;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
repository.getATRepository().save(atStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testSaveATStateWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight - 2;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
// Clear data
ATStateData newAtStateData = new ATStateData(atStateData.getATAddress(),
atStateData.getHeight(),
/*StateData*/ null,
atStateData.getStateHash(),
atStateData.getFees(),
atStateData.isInitial());
repository.getATRepository().save(newAtStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNull(atStateData.getStateData());
}
}
private byte[] buildSimpleAT() {
// Pretend we use 4 values in data segment
int addrCounter = 4;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
}

View File

@@ -13,7 +13,7 @@ fi
cd ${git_dir}
# 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}