Compare commits

...

45 Commits
v1.0 ... v1.0.5

Author SHA1 Message Date
catbref
0594bdf1c7 Bump to v1.0.5 in pom.xml. Filter out build/test artifacts from shaded JAR. 2020-03-24 11:17:43 +00:00
catbref
72c299a331 Add SysTray notification for DB backup. More translations.
Added a setting "showBackupNotification", which is false by default,
that shows a tray notification when a repository backup occurs.

Above notification, and the auto-update notification, now refer to
the SysTray i18n translation lookup resources.
2020-03-24 09:26:40 +00:00
catbref
0b42a7ad63 Modify minOutboundPeers, maxPeers and maxNetworkThreadPoolSize default settings
Typical users don't need quite so many connections, so minOutboundPeers and
maxPeers reduced accordingly.

maxNetworkThreadPoolSize increased from 10 to 20.
2020-03-24 09:24:22 +00:00
catbref
51e59f6ab7 Change HSQLDB repository log fsync() interval from 500ms to 5s 2020-03-24 09:23:17 +00:00
catbref
38394de661 Reduce peer response timeout from 5s to 2s
5s is way too long, and even 2s might still be considered excessive.
However, reducing the timeout might also reduce the number of
network engine "spawn failures" due to too many threads tied up
waiting for ping responses from overloaded peers.

Does not affect peer handshaking: that has a separate timeout.
2020-03-24 09:20:50 +00:00
catbref
22f9755f4f Performance optimizations. Accounts/NTP/System.currentTimeMillis, etc.
Added Ed25519 private key to public key function accessible from SQL.
Added Ed25519 public key to Qortal address function accessible from SQL.

Used above functions to store minting account public key in SQL to
reduce the number of unnecessarily repeated Ed25519 conversions.

Used above functions to store reward-share minting's accounts address
to reduce the number of unneccessarily repeated PK-to-address conversions.

Reduced the usage of PublicKeyAccount to simply Account where possible,
to reduce the number of Ed25519 conversions.

Account.canMint(), Account.canRewardShare() and Account.getEffectiveMintingLevel()
now only perform 1 repository fetch instead of potentially 2 or more.

Cleaned up NTP main thread to reduce CPU load.
A fixed offset can be applied to NTP.getTime() responses, for both
scenarios when NTP is running or not. Useful for testing or simulating
distant remote peers.

Controller.onNetworkMessage() and Network.onMessage() have both had their
complexity simplified by extracting per-case code to separate methods.

Network's EPC engine's thread pool size no longer hard-coded, but comes
from Settings.maxNetworkThreadPoolSize, which is still 10 by default,
but can be increased for high-availability nodes.

Network's EPC task-producing code streamlined to reduce CPU load.

Generally reduced calls to System.currentTimeMillis(), especially
where the value would only be used in verbose logging situations,
and especially in high-call-volume methods, like within repository.
2020-03-23 11:14:05 +00:00
catbref
4cb2e113cb Log (and discard) duplicate outbound connections to the same peer 2020-03-23 11:07:08 +00:00
catbref
e0f024ef5c Performance improvements in networking ExecuteProduceConsume engine
Keep track of when EPC engine can't spawn a new thread as this
might indicate thread-pool exhaustion and cause some network
messages to be lost.

If logging level is NOT 'trace' (or 'all') then don't call
System.currentTimeMillis() as we'll never use the value.

Similarly, don't set thread names if not logging at 'trace' either.

Update EPC tests, particularly unified per-second/end-of-test stats
reporting.
2020-03-23 11:00:19 +00:00
catbref
f95cb99cdc Add support for logging PROOF network message calculation time 2020-03-23 10:57:23 +00:00
catbref
1f0170bb4b Increase timeout for transaction submission via API POST /transactions/process from 500ms to 30s 2020-03-23 10:54:35 +00:00
catbref
5eafdf3c80 Bump version to 1.0.4 2020-03-19 14:30:02 +00:00
catbref
d7c26c27e1 Add debugging message to Peer regarding lost PING replies 2020-03-19 14:29:47 +00:00
catbref
2d18dd62eb Translation / API response improvements 2020-03-19 14:21:48 +00:00
catbref
51fd177d79 Add access to network engine stats
Added API call GET /peers/enginestats to allow external monitoring.

Extract all engine stats in one synchronized block, instead of
separate calls, for better consistency.
2020-03-19 11:19:49 +00:00
catbref
c4643538f1 Improve Synchronizer to reduce network load
Synchronizer now bails out early when trying to find common block with
a peer. There's no need to keep searching if common block is too far
behind that a TOO_DIVERGENT result would be returned.

fetchSummariesFromCommonBlock() reworked to return a useful
SynchronizationResult directly instead of caller trying to infer
what happened based on null/empty returned list!
2020-03-19 11:11:35 +00:00
catbref
0edadaf901 Remove warning 2020-03-19 11:11:05 +00:00
catbref
c05533fb71 Fix comment 2020-03-19 11:10:40 +00:00
catbref
db270f559f Improve minting/syncing status reporting
Added API call GET /admin/status which reports whether minting
is possible (have minting keys & up-to-date) and whether node is
currently attempting to sync.

Corresponding change to system tray mouseover text.

Corresponding text added to SysTray transaction resources.
2020-03-19 11:07:56 +00:00
catbref
79f7f68b0c Change when BlockMinter decides it's ok to mint a block
Previously BlockMinter would attempt to mint if there were at least
'minBlockchainPeers' connected peers and none of them had an
up-to-date block and we did. This was maybe useful for minting block 2
but possibly causes minting chain islands where a badly connected
node mints by itself, even though connected to not up-to-date peers.

Now BlockMinter requires 'minBlockchainPeers' up-to-date peers, not
simply just connected. This should let synchronization bring the
node up-to-date but does require the node to have better peers.

Currently, the default for minBlockchainPeers is 10. So a node
requires 10 up-to-date peers before it will consider minting. It
might be possible to reduce this in the future to lessen network load.
2020-03-19 10:58:53 +00:00
catbref
d30d61edab Reworking/speed-ups for block rewards & general account DB manipulation
**NOTE** currently under wider test - maybe not be final version!
2020-03-18 18:04:45 +00:00
catbref
f7e2ee383e More complete testing of block reward distribution to founders 2020-03-18 18:03:56 +00:00
catbref
544fdbfbe9 Add database-level CHECK constraint on account balances 2020-03-18 18:03:13 +00:00
catbref
c3d1ecb7e1 Reduce maximum allowed distance back to common block 2020-03-18 18:02:00 +00:00
catbref
873a9d0cee Add support for testing with multiple online accounts 2020-03-18 18:00:46 +00:00
catbref
95cb5f607b Use HSQLDB v2.5.0 but with fix for INSERT...ON DUPLICATE KEY UPDATE bug 2020-03-18 17:58:16 +00:00
catbref
54d0b721c4 Dynamically allocate/deallocate Peer byteBuffer to reduce memory load at the expense of extra GC 2020-03-16 17:50:49 +00:00
catbref
4a4678b331 Immediately close socketChannels after accepting peers we won't use 2020-03-16 16:07:17 +00:00
catbref
12f9ecaaca Faster Synchronizer shutdown by checking Controller.isStopping() 2020-03-16 16:05:27 +00:00
catbref
1d3ee77fb8 Increase default number of peers required before a node can mint/sync, and other settings 2020-03-15 14:14:58 +00:00
catbref
64055e280d Shutdown controller, and hence entire node, if networking or API fail to start. 2020-03-11 15:46:59 +00:00
catbref
90e0f9dddc Fix system-dependent path separator usage.
Although HSQLDB is happy being given unix-style path separator '/'
and converting as necessary on other platforms (e.g. Windows),
manipulation of repository pathnames in Java, outside of HSQLDB,
needs to use platform-specific path separators.

Thus, changes made to replace '/' with File.separator where
necessary.

This should fix repository rebuild errors, which then lead to odd
start-up errors like:

2020-03-11 13:55:19 INFO  Controller:270 - Starting repository
2020-03-11 13:55:20 INFO  Controller:287 - Validating blockchain
2020-03-11 13:55:20 INFO  HSQLDBRepository:227 - Rebuilding repository from scratch
2020-03-11 13:55:20 INFO  GenesisBlock:296 - Using genesis block timestamp of 1583870000000
2020-03-11 13:55:21 WARN  HSQLDBRepository:720 - Uncommitted changes (882) after connection close, session [3]
java.lang.NullPointerException
    at org.qortal.transform.block.BlockTransformer.decodeOnlineAccounts(BlockTransformer.java:422)
    at org.qortal.block.Block.getExpandedAccounts(Block.java:546)
    at org.qortal.block.Block.increaseAccountLevels(Block.java:1245)
    at org.qortal.block.Block.increaseAccountLevels(Block.java:1239)
    at org.qortal.block.Block.process(Block.java:1206)
    at org.qortal.block.GenesisBlock.process(GenesisBlock.java:345)
    at org.qortal.block.BlockChain.rebuildBlockchain(BlockChain.java:526)
    at org.qortal.block.BlockChain.validate(BlockChain.java:481)
    at org.qortal.controller.Controller.main(Controller.java:289)

The above happens because the old blockchain still exists when trying to process
the genesis block.
2020-03-11 15:25:04 +00:00
catbref
b0b0e2ac18 Strip JNI options before calling ApplyUpdate
AdvancedInstaller's Java launcher EXE seems to use JNI to launch
the JAR, instead of using the command-line 'java' binary directly.

When AI's launcher does this, it adds options like "abort" and "exit",
along with corresponding hook addresses.

These options are returned by the call to
ManagementFactory.getRuntimeMXBean().getInputArguments() which is
done in AutoUpdate while building the command line for launching
ApplyUpdate.

Because command-line 'java' binary doesn't support these options,
they are now stripped out.
2020-03-11 10:41:39 +00:00
catbref
9db606af5a Latest genesis block 2020-03-10 18:12:11 +00:00
catbref
5bfc17bd64 Remove node UI and have tray icon open local/remove UI server
No more "node UI". UI provided by 3rd party.

"Open UI" tray icon menu item now attempts to open UI at various
local servers (see Settings.uilocalServers) or some random
remote server (Settings.uiRemoteServers).

Default UI port now 12388 (Settings.uiPort).
2020-03-10 13:32:10 +00:00
catbref
a3c44428d3 Restrict TRANSFER_PRIVS recipients to new (non-existent) accounts. 2020-03-04 15:41:47 +00:00
catbref
450ff7318f Slightly more restrictive API access 2020-03-04 15:41:19 +00:00
catbref
2dffd382ae Pre-launch blockchain config / genesis block
Also included: auto-update approval script for use by dev-group admins
(won't work for anyone else)
2020-03-03 15:58:08 +00:00
catbref
e425fe5d5a Translation fixes
Translator class no longer logs warnings for every failed translation.

Commented out unused ApiError enum entries.
Renamed some ApiError names like "_NO_EXISTS" to "_UNKNOWN".
Removed old src/main/resources/globalization/* files.

Added CheckTranslations test app.

Fixed some extraneous/missing ApiError aspects in some API-related classes.
e.g. added NAME_UNKNOWN to GET /names/{name}
2020-03-02 12:49:15 +00:00
catbref
a68caa2de1 Fix serialization of negative BigDecimal values!
We've never needed this before but now it's fixed.
Added corresponding +ve & -ve tests just to make sure.

Only actual use-case that comes to mind is cancelling reward-share.
2020-02-24 13:51:01 +00:00
catbref
3470b8bf57 Added API call GET /transactions/reference/{reference} for looking up a transaction by its reference 2020-02-24 13:50:34 +00:00
catbref
99e11d1f52 Actually respect 'autoUpdateEnabled' setting 2020-02-24 13:49:21 +00:00
catbref
743db9190e Reworked/improved auto-update build/publish tools.
Instead of working on a single 'auto-update' branch,
that needed to be rebased from master branch's HEAD,
we create an orphan branch named after master:HEAD
containing on the update file.

This allows us to keep/delete updates on a per-commit
basis and shows which commit each update is based upon.

Added loads more checking.
2020-02-24 11:22:13 +00:00
catbref
53b3d09288 Fix rebase-and-rebuild.sh to use org.qortal.XorUpdate instead of old org.qora.XorUpdate 2020-02-20 08:30:47 +00:00
catbref
4565d0ddcb Check auto-update can be fetched before broadcasting auto-update TX 2020-02-20 08:28:28 +00:00
catbref
de51de1819 Tools for building, and broadcasting, auto-update 2020-02-20 08:28:28 +00:00
70 changed files with 3105 additions and 2283 deletions

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.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.5.0-fixed</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<versioning>
<release>2.5.0-fixed</release>
<versions>
<version>2.5.0-fixed</version>
</versions>
<lastUpdated>20200318133132</lastUpdated>
</versioning>
</metadata>

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.0</version>
<version>1.0.5</version>
<packaging>jar</packaging>
<properties>
<bitcoin.version>0.15.4</bitcoin.version>
@@ -13,7 +13,7 @@
<commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version>
<guava.version>28.1-jre</guava.version>
<hsqldb.version>2.5.0</hsqldb.version>
<hsqldb.version>2.5.0-fixed</hsqldb.version>
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
<jersey.version>2.29.1</jersey.version>
<jetty.version>9.4.22.v20191022</jetty.version>
@@ -257,6 +257,8 @@
<!-- Don't include original swagger-UI as we're including our own
modified version -->
<exclude>org.webjars:swagger-ui</exclude>
<!-- Don't include JUnit as it's for testing only! -->
<exclude>junit:junit</exclude>
</excludes>
</artifactSet>
<filters>
@@ -379,12 +381,14 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<scope>provided</scope><!-- needed for build, not for runtime -->
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.bohnman/package-info-maven-plugin -->
<dependency>
<groupId>com.github.bohnman</groupId>
<artifactId>package-info-maven-plugin</artifactId>
<version>${package-info-maven-plugin.version}</version>
<scope>provided</scope><!-- needed for build, not for runtime -->
</dependency>
<!-- HSQLDB for repository -->
<dependency>

View File

@@ -204,11 +204,15 @@ public class Account {
* @throws DataException
*/
public boolean canMint() throws DataException {
Integer level = this.getLevel();
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
if (accountData == null)
return false;
Integer level = accountData.getLevel();
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
return true;
if (this.isFounder())
if (Account.isFounder(accountData.getFlags()))
return true;
return false;
@@ -226,11 +230,15 @@ public class Account {
* @throws DataException
*/
public boolean canRewardShare() throws DataException {
Integer level = this.getLevel();
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
if (accountData == null)
return false;
Integer level = accountData.getLevel();
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
return true;
if (this.isFounder())
if (Account.isFounder(accountData.getFlags()))
return true;
return false;
@@ -264,10 +272,14 @@ public class Account {
* @throws DataException
*/
public int getEffectiveMintingLevel() throws DataException {
if (this.isFounder())
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
if (accountData == null)
return 0;
if (Account.isFounder(accountData.getFlags()))
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
Integer level = this.getLevel();
Integer level = accountData.getLevel();
if (level == null)
return 0;
@@ -290,7 +302,7 @@ public class Account {
if (rewardShareData == null)
return 0;
PublicKeyAccount rewardShareMinter = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
return rewardShareMinter.getEffectiveMintingLevel();
}

View File

@@ -7,10 +7,10 @@ import java.util.Map;
public enum ApiError {
// COMMON
UNKNOWN(0, 500),
// UNKNOWN(0, 500),
JSON(1, 400),
NO_BALANCE(2, 422),
NOT_YET_RELEASED(3, 422),
// NO_BALANCE(2, 422),
// NOT_YET_RELEASED(3, 422),
UNAUTHORIZED(4, 403),
REPOSITORY_ISSUE(5, 500),
NON_PRODUCTION(6, 403),
@@ -19,28 +19,28 @@ public enum ApiError {
// VALIDATION
INVALID_SIGNATURE(101, 400),
INVALID_ADDRESS(102, 400),
INVALID_SEED(103, 400),
INVALID_AMOUNT(104, 400),
INVALID_FEE(105, 400),
INVALID_SENDER(106, 400),
INVALID_RECIPIENT(107, 400),
INVALID_NAME_LENGTH(108, 400),
INVALID_VALUE_LENGTH(109, 400),
INVALID_NAME_OWNER(110, 400),
INVALID_BUYER(111, 400),
// INVALID_SEED(103, 400),
// INVALID_AMOUNT(104, 400),
// INVALID_FEE(105, 400),
// INVALID_SENDER(106, 400),
// INVALID_RECIPIENT(107, 400),
// INVALID_NAME_LENGTH(108, 400),
// INVALID_VALUE_LENGTH(109, 400),
// INVALID_NAME_OWNER(110, 400),
// INVALID_BUYER(111, 400),
INVALID_PUBLIC_KEY(112, 400),
INVALID_OPTIONS_LENGTH(113, 400),
INVALID_OPTION_LENGTH(114, 400),
// INVALID_OPTIONS_LENGTH(113, 400),
// INVALID_OPTION_LENGTH(114, 400),
INVALID_DATA(115, 400),
INVALID_DATA_LENGTH(116, 400),
INVALID_UPDATE_VALUE(117, 400),
KEY_ALREADY_EXISTS(118, 422),
KEY_NOT_EXISTS(119, 404),
LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
FEE_LESS_REQUIRED(121, 422),
WALLET_NOT_IN_SYNC(122, 422),
// INVALID_DATA_LENGTH(116, 400),
// INVALID_UPDATE_VALUE(117, 400),
// KEY_ALREADY_EXISTS(118, 422),
// KEY_NOT_EXISTS(119, 404),
// LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
// FEE_LESS_REQUIRED(121, 422),
// WALLET_NOT_IN_SYNC(122, 422),
INVALID_NETWORK_ADDRESS(123, 404),
ADDRESS_NO_EXISTS(124, 404),
ADDRESS_UNKNOWN(124, 404),
INVALID_CRITERIA(125, 400),
INVALID_REFERENCE(126, 400),
TRANSFORMATION_ERROR(127, 400),
@@ -49,72 +49,72 @@ public enum ApiError {
CANNOT_MINT(130, 400),
// WALLET
WALLET_NO_EXISTS(201, 404),
WALLET_ADDRESS_NO_EXISTS(202, 404),
WALLET_LOCKED(203, 422),
WALLET_ALREADY_EXISTS(204, 422),
WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
// WALLET_NO_EXISTS(201, 404),
// WALLET_ADDRESS_NO_EXISTS(202, 404),
// WALLET_LOCKED(203, 422),
// WALLET_ALREADY_EXISTS(204, 422),
// WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
// BLOCKS
BLOCK_NO_EXISTS(301, 404),
BLOCK_UNKNOWN(301, 404),
// TRANSACTIONS
TRANSACTION_NO_EXISTS(311, 404),
TRANSACTION_UNKNOWN(311, 404),
PUBLIC_KEY_NOT_FOUND(304, 404),
TRANSACTION_INVALID(312, 400),
// NAMING
NAME_NO_EXISTS(401, 404),
NAME_ALREADY_EXISTS(402, 422),
NAME_ALREADY_FOR_SALE(403, 422),
NAME_NOT_LOWER_CASE(404, 422),
NAME_SALE_NO_EXISTS(410, 404),
BUYER_ALREADY_OWNER(411, 422),
NAME_UNKNOWN(401, 404),
// NAME_ALREADY_EXISTS(402, 422),
// NAME_ALREADY_FOR_SALE(403, 422),
// NAME_NOT_LOWER_CASE(404, 422),
// NAME_SALE_NO_EXISTS(410, 404),
// BUYER_ALREADY_OWNER(411, 422),
// POLLS
POLL_NO_EXISTS(501, 404),
POLL_ALREADY_EXISTS(502, 422),
DUPLICATE_OPTION(503, 422),
POLL_OPTION_NO_EXISTS(504, 404),
ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
// POLL_NO_EXISTS(501, 404),
// POLL_ALREADY_EXISTS(502, 422),
// DUPLICATE_OPTION(503, 422),
// POLL_OPTION_NO_EXISTS(504, 404),
// ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
// ASSET
INVALID_ASSET_ID(601, 400),
INVALID_ORDER_ID(602, 400),
ORDER_NO_EXISTS(603, 404),
ORDER_UNKNOWN(603, 404),
// NAME PAYMENTS
NAME_NOT_REGISTERED(701, 422),
NAME_FOR_SALE(702, 422),
NAME_WITH_SPACE(703, 422),
// NAME_NOT_REGISTERED(701, 422),
// NAME_FOR_SALE(702, 422),
// NAME_WITH_SPACE(703, 422),
// ATs
INVALID_DESC_LENGTH(801, 400),
EMPTY_CODE(802, 400),
DATA_SIZE(803, 400),
NULL_PAGES(804, 400),
INVALID_TYPE_LENGTH(805, 400),
INVALID_TAGS_LENGTH(806, 400),
INVALID_CREATION_BYTES(809, 400),
// INVALID_DESC_LENGTH(801, 400),
// EMPTY_CODE(802, 400),
// DATA_SIZE(803, 400),
// NULL_PAGES(804, 400),
// INVALID_TYPE_LENGTH(805, 400),
// INVALID_TAGS_LENGTH(806, 400),
// INVALID_CREATION_BYTES(809, 400),
// BLOG/Namestorage
BODY_EMPTY(901, 400),
BLOG_DISABLED(902, 403),
NAME_NOT_OWNER(903, 422),
TX_AMOUNT(904, 400),
BLOG_ENTRY_NO_EXISTS(905, 404),
BLOG_EMPTY(906, 404),
POSTID_EMPTY(907, 400),
POST_NOT_EXISTING(908, 404),
COMMENTING_DISABLED(909, 403),
COMMENT_NOT_EXISTING(910, 404),
INVALID_COMMENT_OWNER(911, 422),
// BODY_EMPTY(901, 400),
// BLOG_DISABLED(902, 403),
// NAME_NOT_OWNER(903, 422),
// TX_AMOUNT(904, 400),
// BLOG_ENTRY_NO_EXISTS(905, 404),
// BLOG_EMPTY(906, 404),
// POSTID_EMPTY(907, 400),
// POST_NOT_EXISTING(908, 404),
// COMMENTING_DISABLED(909, 403),
// COMMENT_NOT_EXISTING(910, 404),
// INVALID_COMMENT_OWNER(911, 422),
// Messages
MESSAGE_FORMAT_NOT_HEX(1001, 400),
MESSAGE_BLANK(1002, 400),
NO_PUBLIC_KEY(1003, 422),
MESSAGESIZE_EXCEEDED(1004, 400),
// MESSAGE_FORMAT_NOT_HEX(1001, 400),
// MESSAGE_BLANK(1002, 400),
// NO_PUBLIC_KEY(1003, 422),
// MESSAGESIZE_EXCEEDED(1004, 400),
// Groups
GROUP_UNKNOWN(1101, 404);

View File

@@ -32,10 +32,13 @@ public class BlockMinterSummary {
}
/** Constructs BlockMinterSummary in reward-share context. */
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String recipientAccount) {
this(mintingAccountPublicKey, blockCount);
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
this.rewardSharePublicKey = rewardSharePublicKey;
this.blockCount = blockCount;
this.mintingAccountPublicKey = mintingAccountPublicKey;
this.mintingAccount = minterAccount;
this.recipientAccount = recipientAccount;
}

View File

@@ -0,0 +1,15 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class NodeStatus {
public boolean isMintingPossible;
public boolean isSynchronizing;
public NodeStatus() {
}
}

View File

@@ -36,8 +36,8 @@ import javax.ws.rs.core.MediaType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
@@ -45,6 +45,7 @@ import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ActivitySummary;
import org.qortal.api.model.NodeInfo;
import org.qortal.api.model.NodeStatus;
import org.qortal.block.BlockChain;
import org.qortal.controller.Controller;
import org.qortal.controller.Synchronizer.SynchronizationResult;
@@ -120,6 +121,27 @@ public class AdminResource {
return nodeInfo;
}
@GET
@Path("/status")
@Operation(
summary = "Fetch node status",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = NodeStatus.class))
)
}
)
public NodeStatus status() {
Security.checkApiCallAllowed(request);
NodeStatus nodeStatus = new NodeStatus();
nodeStatus.isMintingPossible = Controller.getInstance().isMintingPossible();
nodeStatus.isSynchronizing = Controller.getInstance().isSynchronizing();
return nodeStatus;
}
@GET
@Path("/stop")
@Operation(
@@ -202,6 +224,8 @@ public class AdminResource {
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<MintingAccountData> getMintingAccounts() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
List<MintingAccountData> mintingAccounts = repository.getAccountRepository().getMintingAccounts();
@@ -216,7 +240,7 @@ public class AdminResource {
// ignore
}
return new MintingAccountData(mintingAccountData.getPrivateKey(), rewardShareData);
return new MintingAccountData(mintingAccountData, rewardShareData);
}).collect(Collectors.toList());
return mintingAccounts;
@@ -246,6 +270,8 @@ public class AdminResource {
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
public String addMintingAccount(String seed58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] seed = Base58.decode(seed58.trim());
@@ -258,11 +284,11 @@ public class AdminResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
// Qortal: check reward-share's minting account is still allowed to mint
PublicKeyAccount rewardShareMintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
Account rewardShareMintingAccount = new Account(repository, rewardShareData.getMinter());
if (!rewardShareMintingAccount.canMint())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.CANNOT_MINT);
MintingAccountData mintingAccountData = new MintingAccountData(seed);
MintingAccountData mintingAccountData = new MintingAccountData(mintingAccount.getPrivateKey(), mintingAccount.getPublicKey());
repository.getAccountRepository().save(mintingAccountData);
repository.saveChanges();
@@ -296,6 +322,8 @@ public class AdminResource {
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
public String deleteMintingAccount(String seed58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] seed = Base58.decode(seed58.trim());

View File

@@ -410,7 +410,7 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_ORDER_ID, ApiError.ORDER_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public OrderData getAssetOrder(@PathParam("orderid") String orderId58) {
// Decode orderID
@@ -424,7 +424,7 @@ public class AssetsResource {
try (final Repository repository = RepositoryManager.getRepository()) {
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
if (orderData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_UNKNOWN);
return orderData;
} catch (DataException e) {
@@ -451,7 +451,7 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_ORDER_ID, ApiError.ORDER_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public List<TradeData> getAssetOrderTrades(@PathParam("orderid") String orderId58, @Parameter(
ref = "limit"
@@ -471,7 +471,7 @@ public class AssetsResource {
try (final Repository repository = RepositoryManager.getRepository()) {
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
if (orderData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_UNKNOWN);
return repository.getAssetRepository().getOrdersTrades(orderId, limit, offset, reverse);
} catch (DataException e) {
@@ -497,7 +497,7 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public List<OrderData> getAccountOrders(@PathParam("address") String address, @QueryParam("includeClosed") boolean includeClosed,
@QueryParam("includeFulfilled") boolean includeFulfilled, @Parameter(
@@ -514,11 +514,11 @@ public class AssetsResource {
AccountData accountData = repository.getAccountRepository().getAccount(address);
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
byte[] publicKey = accountData.getPublicKey();
if (publicKey == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
return repository.getAssetRepository().getAccountsOrders(publicKey, includeClosed, includeFulfilled, limit, offset, reverse);
} catch (ApiException e) {
@@ -546,7 +546,7 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public List<OrderData> getAccountAssetPairOrders(@PathParam("address") String address, @PathParam("assetid") int assetId,
@PathParam("otherassetid") int otherAssetId, @QueryParam("isClosed") Boolean isClosed, @QueryParam("isFulfilled") Boolean isFulfilled, @Parameter(
@@ -563,11 +563,11 @@ public class AssetsResource {
AccountData accountData = repository.getAccountRepository().getAccount(address);
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
byte[] publicKey = accountData.getPublicKey();
if (publicKey == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);

View File

@@ -59,7 +59,7 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public BlockData getBlock(@PathParam("signature") String signature58) {
// Decode signature
@@ -98,7 +98,7 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(
ref = "limit"
@@ -117,7 +117,7 @@ public class BlocksResource {
try (final Repository repository = RepositoryManager.getRepository()) {
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
} catch (ApiException e) {
@@ -144,13 +144,11 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.REPOSITORY_ISSUE
})
public BlockData getFirstBlock() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().fromHeight(1);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -173,13 +171,11 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.REPOSITORY_ISSUE
})
public BlockData getLastBlock() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getLastBlock();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -202,7 +198,7 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public BlockData getChild(@PathParam("signature") String signature58) {
// Decode signature
@@ -218,13 +214,13 @@ public class BlocksResource {
// Check block exists
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
// Check child block exists
if (childBlockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return childBlockData;
} catch (ApiException e) {
@@ -282,7 +278,7 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public int getHeight(@PathParam("signature") String signature58) {
// Decode signature
@@ -298,7 +294,7 @@ public class BlocksResource {
// Check block exists
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData.getHeight();
} catch (ApiException e) {
@@ -325,13 +321,13 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public BlockData getByHeight(@PathParam("height") int height) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
} catch (ApiException e) {
@@ -357,17 +353,17 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public BlockData getByTimestamp(@PathParam("timestamp") long timestamp) {
try (final Repository repository = RepositoryManager.getRepository()) {
int height = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
if (height == 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
BlockData blockData = repository.getBlockRepository().fromHeight(height);
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
} catch (ApiException e) {
@@ -396,7 +392,7 @@ public class BlocksResource {
}
)
@ApiErrors({
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.REPOSITORY_ISSUE
})
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
ref = "count"
@@ -414,8 +410,6 @@ public class BlocksResource {
}
return blocks;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -23,6 +23,7 @@ import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.NameSummary;
import org.qortal.crypto.Crypto;
@@ -122,10 +123,17 @@ public class NamesResource {
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public NameData getName(@PathParam("name") String name) {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getNameRepository().fromName(name);
NameData nameData = repository.getNameRepository().fromName(name);
if (nameData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NAME_UNKNOWN);
return nameData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -31,6 +31,7 @@ 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;
@Path("/peers")
@Tag(name = "Peers")
@@ -108,6 +109,29 @@ public class PeersResource {
return Network.getInstance().getSelfPeers();
}
@GET
@Path("/enginestats")
@Operation(
summary = "Fetch statistics snapshot for networking engine",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
array = @ArraySchema(
schema = @Schema(
implementation = ExecuteProduceConsume.StatsSnapshot.class
)
)
)
)
}
)
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
Security.checkApiCallAllowed(request);
return Network.getInstance().getStatsSnapshot();
}
@POST
@Operation(
summary = "Add new peer address",
@@ -137,7 +161,7 @@ public class PeersResource {
}
)
@ApiErrors({
ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
public String addPeer(String address) {
Security.checkApiCallAllowed(request);
@@ -151,7 +175,7 @@ public class PeersResource {
return "true";
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_NETWORK_ADDRESS);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@@ -188,7 +212,7 @@ public class PeersResource {
}
)
@ApiErrors({
ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
public String removePeer(String address) {
Security.checkApiCallAllowed(request);
@@ -199,7 +223,7 @@ public class PeersResource {
boolean wasKnown = Network.getInstance().forgetPeer(peerAddress);
return wasKnown ? "true" : "false";
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_NETWORK_ADDRESS);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@@ -223,7 +247,7 @@ public class PeersResource {
}
)
@ApiErrors({
ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
ApiError.REPOSITORY_ISSUE
})
public String removeKnownPeers(String address) {
Security.checkApiCallAllowed(request);
@@ -232,8 +256,6 @@ public class PeersResource {
int numDeleted = Network.getInstance().forgetAllPeers();
return numDeleted != 0 ? "true" : "false";
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -69,9 +69,9 @@ public class TransactionsResource {
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public TransactionData getTransaction(@PathParam("signature") String signature58) {
public TransactionData getTransactionBySignature(@PathParam("signature") String signature58) {
byte[] signature;
try {
signature = Base58.decode(signature58);
@@ -82,7 +82,7 @@ public class TransactionsResource {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
return transactionData;
} catch (ApiException e) {
@@ -110,9 +110,9 @@ public class TransactionsResource {
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE, ApiError.TRANSFORMATION_ERROR
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_UNKNOWN, ApiError.REPOSITORY_ISSUE, ApiError.TRANSFORMATION_ERROR
})
public String getRawTransaction(@PathParam("signature") String signature58) {
public String getRawTransactionBySignature(@PathParam("signature") String signature58) {
byte[] signature;
try {
signature = Base58.decode(signature58);
@@ -123,7 +123,7 @@ public class TransactionsResource {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
byte[] transactionBytes = TransactionTransformer.toBytes(transactionData);
@@ -137,6 +137,46 @@ public class TransactionsResource {
}
}
@GET
@Path("/reference/{reference}")
@Operation(
summary = "Fetch transaction using transaction reference",
description = "Returns transaction",
responses = {
@ApiResponse(
description = "a transaction",
content = @Content(
schema = @Schema(
implementation = TransactionData.class
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_REFERENCE, ApiError.TRANSACTION_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public TransactionData getTransactionByReference(@PathParam("reference") String reference58) {
byte[] reference;
try {
reference = Base58.decode(reference58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromReference(reference);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
return transactionData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/block/{signature}")
@Operation(
@@ -156,7 +196,7 @@ public class TransactionsResource {
}
)
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(
ref = "limit"
@@ -175,7 +215,7 @@ public class TransactionsResource {
try (final Repository repository = RepositoryManager.getRepository()) {
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
} catch (ApiException e) {
@@ -430,7 +470,7 @@ public class TransactionsResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock(500, TimeUnit.MILLISECONDS))
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS))
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
try {
@@ -533,7 +573,7 @@ public class TransactionsResource {
public static ApiException createTransactionInvalidException(HttpServletRequest request, ValidationResult result) {
String translatedResult = Translator.INSTANCE.translate("TransactionValidity", request.getLocale().getLanguage(), result.name());
return ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID, null, translatedResult);
return ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID, null, translatedResult, result.name());
}
}

View File

@@ -9,7 +9,6 @@ import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -126,31 +125,43 @@ public class Block {
protected BigDecimal ourAtFees; // Generated locally
/** Lazy-instantiated expanded info on block's online accounts. */
class ExpandedAccount {
final RewardShareData rewardShareData;
final boolean isRecipientAlsoMinter;
static class ExpandedAccount {
private static final BigDecimal oneHundred = BigDecimal.valueOf(100L);
final Account mintingAccount;
final AccountData mintingAccountData;
final boolean isMinterFounder;
private final Repository repository;
final Account recipientAccount;
final AccountData recipientAccountData;
final boolean isRecipientFounder;
private final RewardShareData rewardShareData;
private final boolean isRecipientAlsoMinter;
private final Account mintingAccount;
private final AccountData mintingAccountData;
private final boolean isMinterFounder;
private final Account recipientAccount;
private final AccountData recipientAccountData;
private final boolean isRecipientFounder;
ExpandedAccount(Repository repository, int accountIndex) throws DataException {
this.repository = repository;
this.rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex);
this.mintingAccount = new PublicKeyAccount(repository, this.rewardShareData.getMinterPublicKey());
this.recipientAccount = new Account(repository, this.rewardShareData.getRecipient());
this.mintingAccount = new Account(repository, this.rewardShareData.getMinter());
this.mintingAccountData = repository.getAccountRepository().getAccount(this.mintingAccount.getAddress());
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
this.recipientAccountData = repository.getAccountRepository().getAccount(this.recipientAccount.getAddress());
this.isRecipientFounder = Account.isFounder(recipientAccountData.getFlags());
this.isRecipientAlsoMinter = this.rewardShareData.getRecipient().equals(this.mintingAccount.getAddress());
this.isRecipientAlsoMinter = this.mintingAccountData.getAddress().equals(this.recipientAccountData.getAddress());
if (this.isRecipientAlsoMinter) {
// Self-share: minter is also recipient
this.recipientAccount = this.mintingAccount;
this.recipientAccountData = this.mintingAccountData;
this.isRecipientFounder = this.isMinterFounder;
} else {
// Recipient differs from minter
this.recipientAccount = new Account(repository, this.rewardShareData.getRecipient());
this.recipientAccountData = repository.getAccountRepository().getAccount(this.recipientAccount.getAddress());
this.isRecipientFounder = Account.isFounder(recipientAccountData.getFlags());
}
}
/**
@@ -176,22 +187,26 @@ public class Block {
}
void distribute(BigDecimal accountAmount) throws DataException {
final BigDecimal oneHundred = BigDecimal.valueOf(100L);
if (this.mintingAccount.getAddress().equals(this.recipientAccount.getAddress())) {
if (this.isRecipientAlsoMinter) {
// minter & recipient the same - simpler case
LOGGER.trace(() -> String.format("Minter/recipient account %s share: %s", this.mintingAccount.getAddress(), accountAmount.toPlainString()));
this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(accountAmount));
if (accountAmount.signum() != 0)
// this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(accountAmount));
this.repository.getAccountRepository().modifyAssetBalance(this.mintingAccount.getAddress(), Asset.QORT, accountAmount);
} else {
// minter & recipient different - extra work needed
BigDecimal recipientAmount = accountAmount.multiply(this.rewardShareData.getSharePercent()).divide(oneHundred, RoundingMode.DOWN);
BigDecimal minterAmount = accountAmount.subtract(recipientAmount);
LOGGER.trace(() -> String.format("Minter account %s share: %s", this.mintingAccount.getAddress(), minterAmount.toPlainString()));
this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(minterAmount));
if (minterAmount.signum() != 0)
// this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(minterAmount));
this.repository.getAccountRepository().modifyAssetBalance(this.mintingAccount.getAddress(), Asset.QORT, minterAmount);
LOGGER.trace(() -> String.format("Recipient account %s share: %s", this.recipientAccount.getAddress(), recipientAmount.toPlainString()));
this.recipientAccount.setConfirmedBalance(Asset.QORT, this.recipientAccount.getConfirmedBalance(Asset.QORT).add(recipientAmount));
if (recipientAmount.signum() != 0)
// this.recipientAccount.setConfirmedBalance(Asset.QORT, this.recipientAccount.getConfirmedBalance(Asset.QORT).add(recipientAmount));
this.repository.getAccountRepository().modifyAssetBalance(this.recipientAccount.getAddress(), Asset.QORT, recipientAmount);
}
}
}
@@ -1256,8 +1271,9 @@ public class Block {
AccountData accountData = getAccountData.apply(expandedAccount);
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
repository.getAccountRepository().setMintedBlockCount(accountData);
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
// repository.getAccountRepository().setMintedBlockCount(accountData); int rowCount = 1; // Until HSQLDB rev 6100 is fixed
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), +1);
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
}
// We are only interested in accounts that are NOT already highest level
@@ -1425,35 +1441,40 @@ public class Block {
public void orphan() throws DataException {
LOGGER.trace(() -> String.format("Orphaning block %d", this.blockData.getHeight()));
// Return AT fees and delete AT states from repository
orphanAtFeesAndStates();
this.repository.setDebug(false);
try {
// Return AT fees and delete AT states from repository
orphanAtFeesAndStates();
// Orphan, and unlink, transactions from this block
orphanTransactionsFromBlock();
// Orphan, and unlink, transactions from this block
orphanTransactionsFromBlock();
// Undo any group-approval decisions that happen at this block
orphanGroupApprovalTransactions();
// Undo any group-approval decisions that happen at this block
orphanGroupApprovalTransactions();
if (this.blockData.getHeight() > 1) {
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
this.cachedExpandedAccounts = null;
if (this.blockData.getHeight() > 1) {
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
this.cachedExpandedAccounts = null;
// Deduct any transaction fees from minter/reward-share account(s)
deductTransactionFees();
// Deduct any transaction fees from minter/reward-share account(s)
deductTransactionFees();
// Block rewards removed after transactions undone
orphanBlockRewards();
// Block rewards removed after transactions undone
orphanBlockRewards();
// Decrease account levels
decreaseAccountLevels();
// Decrease account levels
decreaseAccountLevels();
}
// Delete orphaned balances
this.repository.getAccountRepository().deleteBalancesFromHeight(this.blockData.getHeight());
// Delete block from blockchain
this.repository.getBlockRepository().delete(this.blockData);
this.blockData.setHeight(null);
} finally {
this.repository.setDebug(false);
}
// Delete orphaned balances
this.repository.getAccountRepository().deleteBalancesFromHeight(this.blockData.getHeight());
// Delete block from blockchain
this.repository.getBlockRepository().delete(this.blockData);
this.blockData.setHeight(null);
}
protected void orphanTransactionsFromBlock() throws DataException {
@@ -1571,8 +1592,9 @@ public class Block {
AccountData accountData = getAccountData.apply(expandedAccount);
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
repository.getAccountRepository().setMintedBlockCount(accountData);
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
// repository.getAccountRepository().setMintedBlockCount(accountData); int rowCount = 1; // Until HSQLDB rev 6100 is fixed
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), -1);
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
}
// We are only interested in accounts that are NOT already lowest level
@@ -1602,8 +1624,22 @@ public class Block {
protected void distributeBlockReward(BigDecimal totalAmount) throws DataException {
LOGGER.trace(() -> String.format("Distributing: %s", totalAmount.toPlainString()));
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
// Distribute according to account level
BigDecimal sharedByLevelAmount = distributeBlockRewardByLevel(totalAmount);
LOGGER.trace(() -> String.format("Shared %s of %s based on account levels", sharedByLevelAmount.toPlainString(), totalAmount.toPlainString()));
// Distribute amongst legacy QORA holders
BigDecimal sharedByQoraHoldersAmount = distributeBlockRewardToQoraHolders(totalAmount);
LOGGER.trace(() -> String.format("Shared %s of %s to legacy QORA holders", sharedByQoraHoldersAmount.toPlainString(), totalAmount.toPlainString()));
// Spread remainder across founder accounts
BigDecimal foundersAmount = totalAmount.subtract(sharedByLevelAmount).subtract(sharedByQoraHoldersAmount);
distributeBlockRewardToFounders(foundersAmount);
}
private BigDecimal distributeBlockRewardByLevel(BigDecimal totalAmount) throws DataException {
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
// Distribute amount across bins
BigDecimal sharedAmount = BigDecimal.ZERO;
@@ -1628,36 +1664,17 @@ public class Block {
}
}
// Distribute share across legacy QORA holders
return sharedAmount;
}
private BigDecimal distributeBlockRewardToQoraHolders(BigDecimal totalAmount) throws DataException {
BigDecimal qoraHoldersAmount = BlockChain.getInstance().getQoraHoldersShare().multiply(totalAmount).setScale(8, RoundingMode.DOWN);
LOGGER.trace(() -> String.format("Legacy QORA holders share of %s: %s", totalAmount.toPlainString(), qoraHoldersAmount.toPlainString()));
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getAssetBalances(Asset.LEGACY_QORA, true);
final boolean isProcessingNotOrphaning = totalAmount.signum() >= 0;
// Filter out qoraHolders who have received max QORT due to holding legacy QORA, (ratio from blockchain config)
BigDecimal qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
Iterator<AccountBalanceData> qoraHoldersIterator = qoraHolders.iterator();
while (qoraHoldersIterator.hasNext()) {
AccountBalanceData qoraHolder = qoraHoldersIterator.next();
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
BigDecimal qortFromQora = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA);
// If we're processing a block, then totalAmount will be positive
if (totalAmount.signum() >= 0) {
BigDecimal maxQortFromQora = qoraHolder.getBalance().divide(qoraPerQortReward, RoundingMode.DOWN);
// Disregard qora holders who have already received maximum qort from holding legacy qora
if (qortFromQora.compareTo(maxQortFromQora) >= 0)
qoraHoldersIterator.remove();
} else {
// We're orphaning a block
// so disregard qora holders who have already had their final qort-from-qora reward (i.e. reward reward block is earlier than this one)
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
if (qortFromQoraData != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight())
qoraHoldersIterator.remove();
}
}
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
BigDecimal totalQoraHeld = BigDecimal.ZERO;
for (int i = 0; i < qoraHolders.size(); ++i)
@@ -1666,6 +1683,7 @@ public class Block {
BigDecimal finalTotalQoraHeld = totalQoraHeld;
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", finalTotalQoraHeld.toPlainString()));
BigDecimal sharedAmount = BigDecimal.ZERO;
for (int h = 0; h < qoraHolders.size(); ++h) {
AccountBalanceData qoraHolder = qoraHolders.get(h);
@@ -1674,12 +1692,16 @@ public class Block {
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, finalHolderReward.toPlainString()));
// Too small to register this time?
if (holderReward.signum() == 0)
continue;
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
BigDecimal newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA).add(holderReward);
// If processing, make sure we don't overpay
if (totalAmount.signum() >= 0) {
if (isProcessingNotOrphaning) {
BigDecimal maxQortFromQora = qoraHolder.getBalance().divide(qoraPerQortReward, RoundingMode.DOWN);
if (newQortFromQoraBalance.compareTo(maxQortFromQora) >= 0) {
@@ -1689,7 +1711,7 @@ public class Block {
holderReward = holderReward.subtract(adjustment);
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
// This is also qora holders final qort-from-qora block
// This is also the QORA holder's final QORT-from-QORA block
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, this.blockData.getHeight());
this.repository.getAccountRepository().save(qortFromQoraData);
@@ -1701,9 +1723,10 @@ public class Block {
// Orphaning
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
if (qortFromQoraData != null) {
// Note use of negate() here as qortFromQora will be negative during orphaning,
// but final qort-from-qora is stored in repository during processing (and hence positive).
BigDecimal adjustment = holderReward.subtract(qortFromQoraData.getFinalQortFromQora().negate());
// Final QORT-from-QORA amount from repository was stored during processing, and hence positive.
// So we use add() here as qortFromQora is negative during orphaning.
// More efficient than holderReward.subtract(final-qort-from-qora.negate())
BigDecimal adjustment = holderReward.add(qortFromQoraData.getFinalQortFromQora());
holderReward = holderReward.subtract(adjustment);
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
@@ -1716,7 +1739,8 @@ public class Block {
}
}
qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(holderReward));
// qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(holderReward));
this.repository.getAccountRepository().modifyAssetBalance(qoraHolder.getAddress(), Asset.QORT, holderReward);
if (newQortFromQoraBalance.signum() > 0)
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
@@ -1727,27 +1751,39 @@ public class Block {
sharedAmount = sharedAmount.add(holderReward);
}
// Spread remainder across founder accounts
BigDecimal foundersAmount = totalAmount.subtract(sharedAmount);
BigDecimal finalSharedAmount = sharedAmount;
return sharedAmount;
}
private void distributeBlockRewardToFounders(BigDecimal foundersAmount) throws DataException {
// Remaining reward portion is spread across all founders, online or not
List<AccountData> founderAccounts = this.repository.getAccountRepository().getFlaggedAccounts(Account.FOUNDER_FLAG);
BigDecimal foundersCount = BigDecimal.valueOf(founderAccounts.size());
BigDecimal perFounderAmount = foundersAmount.divide(foundersCount, RoundingMode.DOWN);
LOGGER.trace(() -> String.format("Shared %s of %s, remaining %s to %d founder%s, %s each",
finalSharedAmount.toPlainString(), totalAmount.toPlainString(),
LOGGER.trace(() -> String.format("Sharing remaining %s to %d founder%s, %s each",
foundersAmount.toPlainString(), founderAccounts.size(), (founderAccounts.size() != 1 ? "s" : ""),
perFounderAmount.toPlainString()));
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
for (int a = 0; a < founderAccounts.size(); ++a) {
Account founderAccount = new Account(this.repository, founderAccounts.get(a).getAddress());
// If founder is minter in any online reward-shares then founder's amount is spread across these, otherwise founder gets whole amount.
/* Fixed version:
List<ExpandedAccount> founderExpandedAccounts = expandedAccounts.stream().filter(
accountInfo -> accountInfo.isMinterFounder &&
accountInfo.mintingAccountData.getAddress().equals(founderAccount.getAddress())
).collect(Collectors.toList());
*/
// Broken version:
List<ExpandedAccount> founderExpandedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isMinterFounder).collect(Collectors.toList());
if (founderExpandedAccounts.isEmpty()) {
// Simple case: no founder-as-minter reward-shares online so founder gets whole amount.
Account founderAccount = new Account(this.repository, founderAccounts.get(a).getAddress());
founderAccount.setConfirmedBalance(Asset.QORT, founderAccount.getConfirmedBalance(Asset.QORT).add(perFounderAmount));
// founderAccount.setConfirmedBalance(Asset.QORT, founderAccount.getConfirmedBalance(Asset.QORT).add(perFounderAmount));
this.repository.getAccountRepository().modifyAssetBalance(founderAccount.getAddress(), Asset.QORT, perFounderAmount);
} else {
// Distribute over reward-shares
BigDecimal perFounderRewardShareAmount = perFounderAmount.divide(BigDecimal.valueOf(founderExpandedAccounts.size()), RoundingMode.DOWN);

View File

@@ -12,7 +12,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.Block.ValidationResult;
import org.qortal.controller.Controller;
import org.qortal.data.account.MintingAccountData;
@@ -123,7 +122,7 @@ public class BlockMinter extends Thread {
continue;
}
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
if (!mintingAccount.canMint()) {
// Minting-account component of reward-share can no longer mint - disregard
madi.remove();
@@ -137,19 +136,18 @@ public class BlockMinter extends Thread {
// Disregard peers that have "misbehaved" recently
peers.removeIf(Controller.hasMisbehaved);
// Don't mint if we don't have enough connected peers as where would the transactions/consensus come from?
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
continue;
// Disregard peers that don't have a recent block
peers.removeIf(Controller.hasNoRecentBlock);
// If we have any peers with a recent block, but our latest block isn't recent
// then we need to synchronize instead of minting.
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
continue;
// If our latest block isn't recent then we need to synchronize instead of minting.
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
continue;
// There are no peers with a recent block and/or our latest block is recent
// There are enough peers with a recent block and our latest block is recent
// so go ahead and mint a block if possible.
isMintingPossible = true;
@@ -159,12 +157,12 @@ public class BlockMinter extends Thread {
newBlocks.clear();
}
// Discard accounts we have already built blocks with
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
// Do we need to build any potential new blocks?
List<PrivateKeyAccount> mintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
// Discard accounts we have blocks for
mintingAccounts.removeIf(account -> newBlocks.stream().anyMatch(newBlock -> newBlock.getMinter().getAddress().equals(account.getAddress())));
for (PrivateKeyAccount mintingAccount : mintingAccounts) {
// First block does the AT heavy-lifting
if (newBlocks.isEmpty()) {
@@ -258,11 +256,10 @@ public class BlockMinter extends Thread {
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
if (rewardShareData != null) {
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
LOGGER.info(String.format("Minted block %d, sig %.8s by %s on behalf of %s",
newBlock.getBlockData().getHeight(),
Base58.encode(newBlock.getBlockData().getSignature()),
mintingAccount.getAddress(),
rewardShareData.getMinter(),
rewardShareData.getRecipient()));
} else {
LOGGER.info(String.format("Minted block %d, sig %.8s by %s",
@@ -341,17 +338,19 @@ public class BlockMinter extends Thread {
this.interrupt();
}
public static void mintTestingBlock(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
public static void mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
if (!BlockChain.getInstance().isTestChain()) {
LOGGER.warn("Ignoring attempt to mint testing block for non-test chain!");
return;
}
// Ensure mintingAccount is 'online' so blocks can be minted
Controller.getInstance().ensureTestingAccountOnline(mintingAccount);
Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
// Make sure we're the only thread modifying the blockchain

View File

@@ -21,6 +21,7 @@ import org.qortal.ApplyUpdate;
import org.qortal.api.ApiRequest;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.globalization.Translator;
import org.qortal.gui.SysTray;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -231,6 +232,10 @@ public class AutoUpdate extends Thread {
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Remove JNI options as they won't be supported by command-line 'java'
// These are typically added by the AdvancedInstaller Java launcher EXE
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
// Call ApplyUpdate using new JAR
javaCmd.addAll(Arrays.asList("-cp", NEW_JAR_FILENAME, ApplyUpdate.class.getCanonicalName()));
@@ -241,7 +246,9 @@ public class AutoUpdate extends Thread {
LOGGER.info(String.format("Applying update with: %s", String.join(" ", javaCmd)));
SysTray.getInstance().showMessage("Auto Update", "Applying automatic update and restarting...", MessageType.INFO);
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"),
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
MessageType.INFO);
new ProcessBuilder(javaCmd).start();

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ package org.qortal.controller;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@@ -39,13 +39,13 @@ public class Synchronizer {
private static final int INITIAL_BLOCK_STEP = 8;
private static final int MAXIMUM_BLOCK_STEP = 500;
private static final int MAXIMUM_COMMON_DELTA = 1440; // XXX move to Settings?
private static final int MAXIMUM_COMMON_DELTA = 240; // XXX move to Settings?
private static final int SYNC_BATCH_SIZE = 200;
private static Synchronizer instance;
public enum SynchronizationResult {
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE;
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE, SHUTTING_DOWN;
}
// Constructors
@@ -93,15 +93,11 @@ public class Synchronizer {
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
List<BlockSummaryData> peerBlockSummaries = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight);
if (peerBlockSummaries == null) {
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
return SynchronizationResult.NO_REPLY;
}
if (peerBlockSummaries.isEmpty()) {
LOGGER.info(String.format("Failure to find common block with peer %s", peer));
return SynchronizationResult.NO_COMMON_BLOCK;
}
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries);
if (findCommonBlockResult != SynchronizationResult.OK)
// Logging performed by fetchSummariesFromCommonBlock() above
return findCommonBlockResult;
// First summary is common block
final BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature());
@@ -129,13 +125,6 @@ public class Synchronizer {
return SynchronizationResult.NOTHING_TO_DO;
}
// If common block is too far behind us then we're on massively different forks so give up.
int minCommonHeight = ourInitialHeight - MAXIMUM_COMMON_DELTA;
if (!force && commonBlockHeight < minCommonHeight) {
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
return SynchronizationResult.TOO_DIVERGENT;
}
// Unless we're doing a forced sync, we might need to compare blocks after common block
if (!force && ourInitialHeight > commonBlockHeight) {
// If our latest block is very old, we're very behind and should ditch our fork.
@@ -154,6 +143,9 @@ public class Synchronizer {
int peerBlockCount = peerHeight - commonBlockHeight;
while (peerBlockSummaries.size() < peerBlockCount) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
int lastSummaryHeight = commonBlockHeight + peerBlockSummaries.size();
byte[] previousSignature;
if (peerBlockSummaries.isEmpty())
@@ -212,6 +204,9 @@ public class Synchronizer {
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
while (ourHeight > commonBlockHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
Block block = new Block(repository, blockData);
block.orphan();
@@ -232,6 +227,9 @@ public class Synchronizer {
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
while (ourHeight < peerHeight && ourHeight < maxBatchHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
// Do we need more signatures?
if (peerBlockSignatures.isEmpty()) {
int numberRequested = maxBatchHeight - ourHeight;
@@ -320,45 +318,59 @@ public class Synchronizer {
* @throws DataException
* @throws InterruptedException
*/
private List<BlockSummaryData> fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight) throws DataException, InterruptedException {
private 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;
List<BlockSummaryData> blockSummaries = null;
int testHeight = Math.max(ourHeight - step, 1);
BlockData testBlockData = null;
List<BlockSummaryData> blockSummariesBatch = null;
while (testHeight >= 1) {
// Are we shutting down?
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
// Fetch our block signature at this height
testBlockData = repository.getBlockRepository().fromHeight(testHeight);
if (testBlockData == null) {
// Not found? But we've locked the blockchain and height is below blockchain's tip!
LOGGER.error("Failed to get block at height lower than blockchain tip during synchronization?");
return null;
return SynchronizationResult.REPOSITORY_ISSUE;
}
// Ask for block signatures since test block's signature
byte[] testSignature = testBlockData.getSignature();
LOGGER.trace(String.format("Requesting %d summar%s after height %d", step, (step != 1 ? "ies": "y"), testHeight));
blockSummaries = this.getBlockSummaries(peer, testSignature, step);
blockSummariesBatch = this.getBlockSummaries(peer, testSignature, step);
if (blockSummaries == null)
if (blockSummariesBatch == null) {
// No response - give up this time
return null;
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
return SynchronizationResult.NO_REPLY;
}
LOGGER.trace(String.format("Received %s summar%s", blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y")));
LOGGER.trace(String.format("Received %s summar%s", blockSummariesBatch.size(), (blockSummariesBatch.size() != 1 ? "ies" : "y")));
// Empty list means remote peer is unaware of test signature OR has no new blocks after test signature
if (!blockSummaries.isEmpty())
if (!blockSummariesBatch.isEmpty())
// We have entries so we have found a common block
break;
// No blocks after genesis block?
// We don't get called for a peer at genesis height so this means NO blocks in common
if (testHeight == 1)
return Collections.emptyList();
if (testHeight == 1) {
LOGGER.info(String.format("Failure to find common block with peer %s", peer));
return SynchronizationResult.NO_COMMON_BLOCK;
}
// If common block is too far behind us then we're on massively different forks so give up.
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
return SynchronizationResult.TOO_DIVERGENT;
}
if (peer.getVersion() >= 2) {
step <<= 1;
@@ -373,20 +385,21 @@ public class Synchronizer {
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
blockSummaries.add(0, testBlockSummary);
blockSummariesFromCommon.add(0, testBlockSummary);
blockSummariesFromCommon.addAll(blockSummariesBatch);
// Trim summaries so that first summary is common block.
// Currently we work back from the end until we hit a block we also have.
// TODO: rewrite as modified binary search!
for (int i = blockSummaries.size() - 1; i > 0; --i) {
if (repository.getBlockRepository().exists(blockSummaries.get(i).getSignature())) {
for (int i = blockSummariesFromCommon.size() - 1; i > 0; --i) {
if (repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature())) {
// Note: index i isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
blockSummaries.subList(0, i).clear();
blockSummariesFromCommon.subList(0, i).clear();
break;
}
}
return blockSummaries;
return SynchronizationResult.OK;
}
private List<BlockSummaryData> getBlockSummaries(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {

View File

@@ -2,10 +2,8 @@ package org.qortal.data.account;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.crypto.Crypto;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -16,14 +14,17 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
public class MintingAccountData {
// Properties
// Never actually displayed by API
@Schema(hidden = true)
@XmlTransient
protected byte[] privateKey;
// Not always present - used by API if not null
@XmlTransient
@Schema(hidden = true)
// Read-only by API, we never ask for it as input
@Schema(accessMode = AccessMode.READ_ONLY)
protected byte[] publicKey;
// Not always present - used by API if not null
protected String mintingAccount;
protected String recipientAccount;
protected String address;
@@ -34,17 +35,17 @@ public class MintingAccountData {
protected MintingAccountData() {
}
public MintingAccountData(byte[] privateKey) {
public MintingAccountData(byte[] privateKey, byte[] publicKey) {
this.privateKey = privateKey;
this.publicKey = PrivateKeyAccount.toPublicKey(privateKey);
this.publicKey = publicKey;
}
public MintingAccountData(byte[] privateKey, RewardShareData rewardShareData) {
this(privateKey);
public MintingAccountData(MintingAccountData srcMintingAccountData, RewardShareData rewardShareData) {
this(srcMintingAccountData.privateKey, srcMintingAccountData.publicKey);
if (rewardShareData != null) {
this.recipientAccount = rewardShareData.getRecipient();
this.mintingAccount = Crypto.toAddress(rewardShareData.getMinterPublicKey());
this.mintingAccount = rewardShareData.getMinter();
} else {
this.address = Crypto.toAddress(this.publicKey);
}
@@ -56,8 +57,6 @@ public class MintingAccountData {
return this.privateKey;
}
@XmlElement(name = "publicKey")
@Schema(accessMode = AccessMode.READ_ONLY)
public byte[] getPublicKey() {
return this.publicKey;
}

View File

@@ -5,8 +5,9 @@ import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import org.qortal.crypto.Crypto;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@@ -14,6 +15,12 @@ public class RewardShareData {
// Properties
private byte[] minterPublicKey;
// "minter" is called "mintingAccount" instead
@XmlTransient
@Schema(hidden = true)
private String minter;
private String recipient;
private byte[] rewardSharePublicKey;
private BigDecimal sharePercent;
@@ -25,8 +32,9 @@ public class RewardShareData {
}
// Used when fetching from repository
public RewardShareData(byte[] minterPublicKey, String recipient, byte[] rewardSharePublicKey, BigDecimal sharePercent) {
public RewardShareData(byte[] minterPublicKey, String minter, String recipient, byte[] rewardSharePublicKey, BigDecimal sharePercent) {
this.minterPublicKey = minterPublicKey;
this.minter = minter;
this.recipient = recipient;
this.rewardSharePublicKey = rewardSharePublicKey;
this.sharePercent = sharePercent;
@@ -38,6 +46,10 @@ public class RewardShareData {
return this.minterPublicKey;
}
public String getMinter() {
return this.minter;
}
public String getRecipient() {
return this.recipient;
}
@@ -52,7 +64,7 @@ public class RewardShareData {
@XmlElement(name = "mintingAccount")
public String getMintingAccount() {
return Crypto.toAddress(this.minterPublicKey);
return this.minter;
}
}

View File

@@ -17,36 +17,32 @@ import org.apache.logging.log4j.Logger;
public enum BIP39WordList {
INSTANCE;
private Logger LOGGER = LogManager.getLogger(BIP39WordList.class);
private static final Logger LOGGER = LogManager.getLogger(BIP39WordList.class);
private Map<String, List<String>> wordListsByLang;
private BIP39WordList() {
wordListsByLang = new HashMap<>();
}
private static final Map<String, List<String>> wordListsByLang = new HashMap<>();
public synchronized List<String> getByLang(String lang) {
List<String> wordList = wordListsByLang.get(lang);
if (wordList == null) {
if (wordList == null && !wordListsByLang.containsKey(lang)) {
ClassLoader loader = this.getClass().getClassLoader();
try (InputStream inputStream = loader.getResourceAsStream("BIP39/wordlist_" + lang + ".txt")) {
if (inputStream == null) {
if (inputStream == null)
LOGGER.warn(String.format("Can't locate '%s' BIP39 wordlist", lang));
return null;
}
wordList = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.toList());
} catch (IOException e) {
LOGGER.warn(String.format("Error reading '%s' BIP39 wordlist", lang), e);
return null;
LOGGER.warn(String.format("Error reading '%s' BIP39 wordlist: %s", lang, e.getMessage()));
}
wordListsByLang.put(lang, wordList);
}
return Collections.unmodifiableList(wordList);
if (wordList != null)
return Collections.unmodifiableList(wordList);
else
return null;
}
}

View File

@@ -6,6 +6,7 @@ import java.util.Map;
import java.util.MissingFormatArgumentException;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -13,35 +14,12 @@ import org.apache.logging.log4j.Logger;
public enum Translator {
INSTANCE;
private final Logger LOGGER = LogManager.getLogger(Translator.class);
private final String DEFAULT_LANG = Locale.getDefault().getLanguage();
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
private static final String DEFAULT_LANG = Locale.getDefault().getLanguage();
private final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
private static final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
private synchronized ResourceBundle getOrLoadResourceBundle(String className, String lang) {
final String bundleKey = className + ":" + lang;
ResourceBundle resourceBundle = resourceBundles.get(bundleKey);
if (resourceBundle != null)
return resourceBundle;
try {
resourceBundle = ResourceBundle.getBundle("i18n." + className, Locale.forLanguageTag(lang));
} catch (MissingResourceException e) {
LOGGER.warn("Can't locate '" + lang + "' translation resource bundle for " + className, e);
return null;
}
resourceBundles.put(bundleKey, resourceBundle);
return resourceBundle;
}
public String translate(final String className, final String key) {
return this.translate(className, DEFAULT_LANG, key);
}
public String translate(final String className, final String lang, final String key, final Object... args) {
public String translate(String className, String lang, String key, Object... args) {
ResourceBundle resourceBundle = getOrLoadResourceBundle(className, lang);
if (resourceBundle == null || !resourceBundle.containsKey(key))
@@ -55,4 +33,37 @@ public enum Translator {
}
}
public String translate(String className, String key) {
return this.translate(className, DEFAULT_LANG, key);
}
public Set<String> keySet(String className, String lang) {
ResourceBundle resourceBundle = getOrLoadResourceBundle(className, lang);
if (resourceBundle == null)
return null;
return resourceBundle.keySet();
}
private synchronized ResourceBundle getOrLoadResourceBundle(String className, String lang) {
String bundleKey = className + ":" + lang;
ResourceBundle resourceBundle = resourceBundles.get(bundleKey);
if (resourceBundle != null || resourceBundles.containsKey(bundleKey))
return resourceBundle;
try {
resourceBundle = ResourceBundle.getBundle("i18n." + className, Locale.forLanguageTag(lang));
} catch (MissingResourceException e) {
LOGGER.warn(String.format("Can't locate '%s' translation resource bundle for %s", lang, className));
// Set to null then fall-through to storing in map so we don't emit warning more than once
resourceBundle = null;
}
resourceBundles.put(bundleKey, resourceBundle);
return resourceBundle;
}
}

View File

@@ -10,11 +10,14 @@ import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -30,7 +33,7 @@ import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
import org.qortal.globalization.Translator;
import org.qortal.settings.Settings;
import org.qortal.ui.UiService;
import org.qortal.utils.RandomizeList;
import org.qortal.utils.URLViewer;
public class SysTray {
@@ -144,15 +147,11 @@ public class SysTray {
}
});
JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_NODE_UI"));
JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_UI"));
openUi.addActionListener(actionEvent -> {
destroyHiddenDialog();
try {
URLViewer.openWebpage(new URL("http://localhost:" + Settings.getInstance().getUiPort()));
} catch (Exception e) {
LOGGER.error("Unable to open node UI in browser");
}
new OpenUiWorker().execute();
});
menu.add(openUi);
@@ -174,7 +173,7 @@ public class SysTray {
syncTime.addActionListener(actionEvent -> {
destroyHiddenDialog();
new SynchronizeWorker().execute();
new SynchronizeClockWorker().execute();
});
menu.add(syncTime);
}
@@ -190,11 +189,53 @@ public class SysTray {
return menu;
}
class SynchronizeWorker extends SwingWorker<Void, Void> {
static class OpenUiWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() {
List<String> uiServers = new ArrayList<>();
String[] remoteUiServers = Settings.getInstance().getRemoteUiServers();
uiServers.addAll(Arrays.asList(remoteUiServers));
// Randomize remote servers
uiServers = RandomizeList.randomize(uiServers);
// Prepend local servers
String[] localUiServers = Settings.getInstance().getLocalUiServers();
uiServers.addAll(0, Arrays.asList(localUiServers));
// Check each server in turn before opening browser tab
int uiPort = Settings.getInstance().getUiServerPort();
for (String uiServer : uiServers) {
InetSocketAddress socketAddress = new InetSocketAddress(uiServer, uiPort);
// If we couldn't resolve try next
if (socketAddress.isUnresolved())
continue;
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.socket().connect(socketAddress, 100);
// If we reach here, then socket connected to UI server!
URLViewer.openWebpage(new URL(String.format("http://%s:%d", uiServer, uiPort)));
return null;
} catch (IOException e) {
// try next server
} catch (Exception e) {
LOGGER.error("Unable to open UI website in browser");
return null;
}
}
return null;
}
}
static class SynchronizeClockWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() {
// Extract reconfiguration script from resources
String resourceName = "/" + UiService.DOWNLOADS_RESOURCE_PATH + "/" + NTP_SCRIPT;
String resourceName = "/node-management/" + NTP_SCRIPT;
Path scriptPath = Paths.get(NTP_SCRIPT);
try (InputStream in = SysTray.class.getResourceAsStream(resourceName)) {
@@ -218,7 +259,7 @@ public class SysTray {
}
}
class ClosingWorker extends SwingWorker<Void, Void> {
static class ClosingWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() {
Controller.getInstance().shutdown();

View File

@@ -62,11 +62,10 @@ public enum Handshake {
// Is this ID already connected inbound or outbound?
Peer otherInboundPeer = Network.getInstance().getInboundPeerWithId(peerId);
Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peerId);
// Extra checks on inbound peers with known IDs, to prevent ID stealing
if (!peer.isOutbound() && otherInboundPeer != null) {
Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peerId);
if (otherOutboundPeer == null) {
// We already have an inbound peer with this ID, but no outgoing peer with which to request verification
LOGGER.trace(String.format("Discarding inbound peer %s with existing ID", peer));
@@ -86,6 +85,11 @@ public enum Handshake {
// Generate verification codes for later
peer.generateVerificationCodes();
}
} else if (peer.isOutbound() && otherOutboundPeer != null) {
// We already have an outbound connection to this peer?
LOGGER.info(String.format("We already have another outbound connection to peer %s - discarding", peer));
// Handshake failure - caller will deal with disconnect
return null;
} else {
// Set peer's ID
peer.setPeerId(peerId);
@@ -231,7 +235,7 @@ public enum Handshake {
private static void sendProof(Peer peer) {
if (peer.isOutbound()) {
// For outbound connections we need to generate real proof
new Proof(peer).start();
new Proof(peer).start(); // Calculate & send in a new thread to free up networking processing
} else {
// For incoming connections we only need to send a fake proof message as confirmation
Message proofMessage = new ProofMessage(peer.getConnectionTimestamp(), 0, 0);

View File

@@ -56,6 +56,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
import org.qortal.utils.NTP;
// For managing peers
@@ -88,9 +89,14 @@ public class Network {
"node4.qortal.org",
"node5.qortal.org",
"node6.qortal.org",
"node7.qortal.org"
"node7.qortal.org",
"node8.qortal.org",
"node9.qortal.org",
"node10.qortal.org"
};
private static final long NETWORK_EPC_KEEPALIVE = 10L; // seconds
public static final int MAX_SIGNATURES_PER_REPLY = 500;
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
public static final int PEER_ID_LENGTH = 128;
@@ -138,9 +144,10 @@ public class Network {
mergePeersLock = new ReentrantLock();
// We'll use a cached thread pool, max 10 threads, but with more aggressive 10 second timeout.
ExecutorService networkExecutor = new ThreadPoolExecutor(1, 10,
10L, TimeUnit.SECONDS,
// We'll use a cached thread pool but with more aggressive timeout.
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
Settings.getInstance().getMaxNetworkThreadPoolSize(),
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
networkEPC = new NetworkProcessor(networkExecutor);
}
@@ -196,6 +203,10 @@ public class Network {
return this.maxMessageSize;
}
public StatsSnapshot getStatsSnapshot() {
return this.networkEPC.getStatsSnapshot();
}
// Peer lists
public List<Peer> getConnectedPeers() {
@@ -294,15 +305,17 @@ public class Network {
if (task != null)
return task;
task = maybeProducePeerPingTask();
final Long now = NTP.getTime();
task = maybeProducePeerPingTask(now);
if (task != null)
return task;
task = maybeProduceConnectPeerTask();
task = maybeProduceConnectPeerTask(now);
if (task != null)
return task;
task = maybeProduceBroadcastTask();
task = maybeProduceBroadcastTask(now);
if (task != null)
return task;
@@ -315,6 +328,65 @@ public class Network {
return null;
}
private Task maybeProducePeerMessageTask() {
for (Peer peer : getConnectedPeers()) {
Task peerTask = peer.getMessageTask();
if (peerTask != null)
return peerTask;
}
return null;
}
private Task maybeProducePeerPingTask(Long now) {
// Ask connected peers whether they need a ping
for (Peer peer : getConnectedPeers()) {
Task peerTask = peer.getPingTask(now);
if (peerTask != null)
return peerTask;
}
return null;
}
class PeerConnectTask implements ExecuteProduceConsume.Task {
private final Peer peer;
public PeerConnectTask(Peer peer) {
this.peer = peer;
}
@Override
public void perform() throws InterruptedException {
connectPeer(peer);
}
}
private Task maybeProduceConnectPeerTask(Long now) throws InterruptedException {
if (now == null || now < nextConnectTaskTimestamp)
return null;
if (getOutboundHandshakedPeers().size() >= minOutboundPeers)
return null;
nextConnectTaskTimestamp = now + 1000L;
Peer targetPeer = getConnectablePeer(now);
if (targetPeer == null)
return null;
// Create connection task
return new PeerConnectTask(targetPeer);
}
private Task maybeProduceBroadcastTask(Long now) {
if (now == null || now < nextBroadcastTimestamp)
return null;
nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
return () -> Controller.getInstance().doNetworkBroadcast();
}
class ChannelTask implements ExecuteProduceConsume.Task {
private final SelectionKey selectionKey;
@@ -397,67 +469,6 @@ public class Network {
return new ChannelTask(nextSelectionKey);
}
private Task maybeProducePeerMessageTask() {
for (Peer peer : getConnectedPeers()) {
Task peerTask = peer.getMessageTask();
if (peerTask != null)
return peerTask;
}
return null;
}
private Task maybeProducePeerPingTask() {
// Ask connected peers whether they need a ping
for (Peer peer : getConnectedPeers()) {
Task peerTask = peer.getPingTask();
if (peerTask != null)
return peerTask;
}
return null;
}
class PeerConnectTask implements ExecuteProduceConsume.Task {
private final Peer peer;
public PeerConnectTask(Peer peer) {
this.peer = peer;
}
@Override
public void perform() throws InterruptedException {
connectPeer(peer);
}
}
private Task maybeProduceConnectPeerTask() throws InterruptedException {
if (getOutboundHandshakedPeers().size() >= minOutboundPeers)
return null;
final Long now = NTP.getTime();
if (now == null || now < nextConnectTaskTimestamp)
return null;
nextConnectTaskTimestamp = now + 1000L;
Peer targetPeer = getConnectablePeer();
if (targetPeer == null)
return null;
// Create connection task
return new PeerConnectTask(targetPeer);
}
private Task maybeProduceBroadcastTask() {
final Long now = NTP.getTime();
if (now == null || now < nextBroadcastTimestamp)
return null;
nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
return () -> Controller.getInstance().doNetworkBroadcast();
}
}
private void acceptConnection(ServerSocketChannel serverSocketChannel) throws InterruptedException {
@@ -478,18 +489,20 @@ public class Network {
try {
if (now == null) {
LOGGER.debug(String.format("Connection discarded from peer %s due to lack of NTP sync", socketChannel.getRemoteAddress()));
LOGGER.debug(() -> String.format("Connection discarded from peer %s due to lack of NTP sync", PeerAddress.fromSocket(socketChannel.socket())));
socketChannel.close();
return;
}
synchronized (this.connectedPeers) {
if (connectedPeers.size() >= maxPeers) {
// We have enough peers
LOGGER.debug(String.format("Connection discarded from peer %s", socketChannel.getRemoteAddress()));
LOGGER.debug(() -> String.format("Connection discarded from peer %s", PeerAddress.fromSocket(socketChannel.socket())));
socketChannel.close();
return;
}
LOGGER.debug(String.format("Connection accepted from peer %s", socketChannel.getRemoteAddress()));
LOGGER.debug(() -> String.format("Connection accepted from peer %s", PeerAddress.fromSocket(socketChannel.socket())));
newPeer = new Peer(socketChannel);
this.connectedPeers.add(newPeer);
@@ -578,9 +591,27 @@ public class Network {
}
}
private Peer getConnectablePeer() throws InterruptedException {
final long now = NTP.getTime();
private final Predicate<PeerData> isSelfPeer = peerData -> {
PeerAddress peerAddress = peerData.getAddress();
return this.selfPeers.stream().anyMatch(selfPeer -> selfPeer.equals(peerAddress));
};
private final Predicate<PeerData> isConnectedPeer = peerData -> {
PeerAddress peerAddress = peerData.getAddress();
return this.connectedPeers.stream().anyMatch(peer -> peer.getPeerData().getAddress().equals(peerAddress));
};
private final Predicate<PeerData> isResolvedAsConnectedPeer = peerData -> {
try {
InetSocketAddress resolvedSocketAddress = peerData.getAddress().toSocketAddress();
return this.connectedPeers.stream().anyMatch(peer -> peer.getResolvedAddress().equals(resolvedSocketAddress));
} catch (UnknownHostException e) {
// Can't resolve - no point even trying to connect
return true;
}
};
private Peer getConnectablePeer(final Long now) throws InterruptedException {
// We can't block here so use tryRepository(). We don't NEED to connect a new peer.
try (final Repository repository = RepositoryManager.tryRepository()) {
if (repository == null)
@@ -596,36 +627,17 @@ public class Network {
peerData.getLastAttempted() > lastAttemptedThreshold);
// Don't consider peers that we know loop back to ourself
Predicate<PeerData> isSelfPeer = peerData -> {
PeerAddress peerAddress = peerData.getAddress();
return this.selfPeers.stream().anyMatch(selfPeer -> selfPeer.equals(peerAddress));
};
synchronized (this.selfPeers) {
peers.removeIf(isSelfPeer);
}
// Don't consider already connected peers (simple address match)
Predicate<PeerData> isConnectedPeer = peerData -> {
PeerAddress peerAddress = peerData.getAddress();
return this.connectedPeers.stream().anyMatch(peer -> peer.getPeerData().getAddress().equals(peerAddress));
};
synchronized (this.connectedPeers) {
peers.removeIf(isConnectedPeer);
}
// Don't consider already connected peers (resolved address match)
Predicate<PeerData> isResolvedAsConnectedPeer = peerData -> {
try {
InetSocketAddress resolvedSocketAddress = peerData.getAddress().toSocketAddress();
return this.connectedPeers.stream().anyMatch(peer -> peer.getResolvedAddress().equals(resolvedSocketAddress));
} catch (UnknownHostException e) {
// Can't resolve - no point even trying to connect
return true;
}
};
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
synchronized (this.connectedPeers) {
peers.removeIf(isResolvedAsConnectedPeer);
}
@@ -725,126 +737,43 @@ public class Network {
Handshake handshakeStatus = peer.getHandshakeStatus();
if (handshakeStatus != Handshake.COMPLETED) {
try {
// Still handshaking
LOGGER.trace(() -> String.format("Handshake status %s, message %s from peer %s", handshakeStatus.name(), (message != null ? message.getType().name() : "null"), peer));
// v1 nodes are keen on sending PINGs early. Send to back of queue so we'll process right after handshake
if (message != null && message.getType() == MessageType.PING) {
peer.queueMessage(message);
return;
}
// Check message type is as expected
if (handshakeStatus.expectedMessageType != null && message.getType() != handshakeStatus.expectedMessageType) {
LOGGER.debug(String.format("Unexpected %s message from %s, expected %s", message.getType().name(), peer, handshakeStatus.expectedMessageType));
peer.disconnect("unexpected message");
return;
}
Handshake newHandshakeStatus = handshakeStatus.onMessage(peer, message);
if (newHandshakeStatus == null) {
// Handshake failure
LOGGER.debug(String.format("Handshake failure with peer %s message %s", peer, message.getType().name()));
peer.disconnect("handshake failure");
return;
}
if (peer.isOutbound())
// If we made outbound connection then we need to act first
newHandshakeStatus.action(peer);
else
// We have inbound connection so we need to respond in kind with what we just received
handshakeStatus.action(peer);
peer.setHandshakeStatus(newHandshakeStatus);
if (newHandshakeStatus == Handshake.COMPLETED)
this.onHandshakeCompleted(peer);
return;
} finally {
peer.resetHandshakeMessagePending();
}
onHandshakingMessage(peer, message, handshakeStatus);
return;
}
// Should be non-handshaking messages from now on
// Ordered by message type value
switch (message.getType()) {
case PEER_VERIFY:
// Remote peer wants extra verification
possibleVerificationResponse(peer);
case GET_PEERS:
onGetPeersMessage(peer, message);
break;
case VERIFICATION_CODES:
VerificationCodesMessage verificationCodesMessage = (VerificationCodesMessage) message;
case PEERS:
onPeersMessage(peer, message);
break;
// Remote peer is sending the code it wants to receive back via our outbound connection to it
Peer ourUnverifiedPeer = Network.getInstance().getInboundPeerWithId(Network.getInstance().getOurPeerId());
ourUnverifiedPeer.setVerificationCodes(verificationCodesMessage.getVerificationCodeSent(), verificationCodesMessage.getVerificationCodeExpected());
possibleVerificationResponse(ourUnverifiedPeer);
case PING:
onPingMessage(peer, message);
break;
case VERSION:
case PEER_ID:
case PROOF:
LOGGER.debug(String.format("Unexpected handshaking message %s from peer %s", message.getType().name(), peer));
LOGGER.debug(() -> String.format("Unexpected handshaking message %s from peer %s", message.getType().name(), peer));
peer.disconnect("unexpected handshaking message");
return;
case PING:
PingMessage pingMessage = (PingMessage) message;
// Generate 'pong' using same ID
PingMessage pongMessage = new PingMessage();
pongMessage.setId(pingMessage.getId());
if (!peer.sendMessage(pongMessage))
peer.disconnect("failed to send ping reply");
break;
case PEERS:
PeersMessage peersMessage = (PeersMessage) message;
List<PeerAddress> peerAddresses = new ArrayList<>();
// v1 PEERS message doesn't support port numbers so we have to add default port
for (InetAddress peerAddress : peersMessage.getPeerAddresses())
// This is always IPv4 so we don't have to worry about bracketing IPv6.
peerAddresses.add(PeerAddress.fromString(peerAddress.getHostAddress()));
// Also add peer's details
peerAddresses.add(PeerAddress.fromString(peer.getPeerData().getAddress().getHost()));
mergePeers(peer.toString(), peerAddresses);
break;
case PEERS_V2:
PeersV2Message peersV2Message = (PeersV2Message) message;
List<PeerAddress> peerV2Addresses = peersV2Message.getPeerAddresses();
// First entry contains remote peer's listen port but empty address.
int peerPort = peerV2Addresses.get(0).getPort();
peerV2Addresses.remove(0);
// If inbound peer, use listen port and socket address to recreate first entry
if (!peer.isOutbound()) {
PeerAddress sendingPeerAddress = PeerAddress.fromString(peer.getPeerData().getAddress().getHost() + ":" + peerPort);
LOGGER.trace(() -> String.format("PEERS_V2 sending peer's listen address: %s", sendingPeerAddress.toString()));
peerV2Addresses.add(0, sendingPeerAddress);
}
mergePeers(peer.toString(), peerV2Addresses);
onPeersV2Message(peer, message);
break;
case GET_PEERS:
// Send our known peers
if (!peer.sendMessage(buildPeersMessage(peer)))
peer.disconnect("failed to send peers list");
case PEER_VERIFY:
onPeerVerifyMessage(peer, message);
break;
case VERIFICATION_CODES:
onVerificationCodesMessage(peer, message);
break;
default:
@@ -854,6 +783,116 @@ public class Network {
}
}
private void onHandshakingMessage(Peer peer, Message message, Handshake handshakeStatus) {
try {
// Still handshaking
LOGGER.trace(() -> String.format("Handshake status %s, message %s from peer %s", handshakeStatus.name(), (message != null ? message.getType().name() : "null"), peer));
// v1 nodes are keen on sending PINGs early. Send to back of queue so we'll process right after handshake
if (message != null && message.getType() == MessageType.PING) {
peer.queueMessage(message);
return;
}
// Check message type is as expected
if (handshakeStatus.expectedMessageType != null && message.getType() != handshakeStatus.expectedMessageType) {
LOGGER.debug(() -> String.format("Unexpected %s message from %s, expected %s", message.getType().name(), peer, handshakeStatus.expectedMessageType));
peer.disconnect("unexpected message");
return;
}
Handshake newHandshakeStatus = handshakeStatus.onMessage(peer, message);
if (newHandshakeStatus == null) {
// Handshake failure
LOGGER.debug(() -> String.format("Handshake failure with peer %s message %s", peer, message.getType().name()));
peer.disconnect("handshake failure");
return;
}
if (peer.isOutbound())
// If we made outbound connection then we need to act first
newHandshakeStatus.action(peer);
else
// We have inbound connection so we need to respond in kind with what we just received
handshakeStatus.action(peer);
peer.setHandshakeStatus(newHandshakeStatus);
if (newHandshakeStatus == Handshake.COMPLETED)
this.onHandshakeCompleted(peer);
} finally {
peer.resetHandshakeMessagePending();
}
}
private void onGetPeersMessage(Peer peer, Message message) {
// Send our known peers
if (!peer.sendMessage(buildPeersMessage(peer)))
peer.disconnect("failed to send peers list");
}
private void onPeersMessage(Peer peer, Message message) {
PeersMessage peersMessage = (PeersMessage) message;
List<PeerAddress> peerAddresses = new ArrayList<>();
// v1 PEERS message doesn't support port numbers so we have to add default port
for (InetAddress peerAddress : peersMessage.getPeerAddresses())
// This is always IPv4 so we don't have to worry about bracketing IPv6.
peerAddresses.add(PeerAddress.fromString(peerAddress.getHostAddress()));
// Also add peer's details
peerAddresses.add(PeerAddress.fromString(peer.getPeerData().getAddress().getHost()));
mergePeers(peer.toString(), peerAddresses);
}
private void onPingMessage(Peer peer, Message message) {
PingMessage pingMessage = (PingMessage) message;
// Generate 'pong' using same ID
PingMessage pongMessage = new PingMessage();
pongMessage.setId(pingMessage.getId());
if (!peer.sendMessage(pongMessage))
peer.disconnect("failed to send ping reply");
}
private void onPeersV2Message(Peer peer, Message message) {
PeersV2Message peersV2Message = (PeersV2Message) message;
List<PeerAddress> peerV2Addresses = peersV2Message.getPeerAddresses();
// First entry contains remote peer's listen port but empty address.
int peerPort = peerV2Addresses.get(0).getPort();
peerV2Addresses.remove(0);
// If inbound peer, use listen port and socket address to recreate first entry
if (!peer.isOutbound()) {
PeerAddress sendingPeerAddress = PeerAddress.fromString(peer.getPeerData().getAddress().getHost() + ":" + peerPort);
LOGGER.trace(() -> String.format("PEERS_V2 sending peer's listen address: %s", sendingPeerAddress.toString()));
peerV2Addresses.add(0, sendingPeerAddress);
}
mergePeers(peer.toString(), peerV2Addresses);
}
private void onPeerVerifyMessage(Peer peer, Message message) {
// Remote peer wants extra verification
possibleVerificationResponse(peer);
}
private void onVerificationCodesMessage(Peer peer, Message message) {
VerificationCodesMessage verificationCodesMessage = (VerificationCodesMessage) message;
// Remote peer is sending the code it wants to receive back via our outbound connection to it
Peer ourUnverifiedPeer = Network.getInstance().getInboundPeerWithId(Network.getInstance().getOurPeerId());
ourUnverifiedPeer.setVerificationCodes(verificationCodesMessage.getVerificationCodeSent(), verificationCodesMessage.getVerificationCodeExpected());
possibleVerificationResponse(ourUnverifiedPeer);
}
private void possibleVerificationResponse(Peer peer) {
// Can't respond if we don't have the codes (yet?)
if (peer.getVerificationCodeExpected() == null)

View File

@@ -43,7 +43,7 @@ public class Peer {
private static final int CONNECT_TIMEOUT = 1000; // ms
/** Maximum time to wait for a message reply to arrive from peer. (ms) */
private static final int RESPONSE_TIMEOUT = 5000; // ms
private static final int RESPONSE_TIMEOUT = 2000; // ms
/**
* Interval between PING messages to a peer. (ms)
@@ -61,6 +61,8 @@ public class Peer {
private InetSocketAddress resolvedAddress = null;
/** True if remote address is loopback/link-local/site-local, false otherwise. */
private boolean isLocal;
private final Object byteBufferLock = new Object();
private volatile ByteBuffer byteBuffer;
private Map<Integer, BlockingQueue<Message>> replyQueues;
private LinkedBlockingQueue<Message> pendingMessages;
@@ -256,7 +258,7 @@ public class Peer {
this.connectionTimestamp = NTP.getTime();
this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
this.socketChannel.configureBlocking(false);
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
this.byteBuffer = null; // Defer allocation to when we need it, to save memory. Sorry GC!
this.replyQueues = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
this.pendingMessages = new LinkedBlockingQueue<>();
}
@@ -292,11 +294,15 @@ public class Peer {
* @throws IOException
*/
/* package */ void readChannel() throws IOException {
synchronized (this.byteBuffer) {
synchronized (this.byteBufferLock) {
while(true) {
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed())
return;
// Do we need to allocate byteBuffer?
if (this.byteBuffer == null)
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
final int bytesRead = this.socketChannel.read(this.byteBuffer);
if (bytesRead == -1) {
this.disconnect("EOF");
@@ -318,9 +324,15 @@ public class Peer {
return;
}
if (message == null && bytesRead == 0 && !wasByteBufferFull)
if (message == null && bytesRead == 0 && !wasByteBufferFull) {
// No complete message in buffer, no more bytes to read from socket even though there was room to read bytes
// If byteBuffer is empty then we can deallocate it, to save memory, albeit costing GC
if (this.byteBuffer.remaining() == this.byteBuffer.capacity())
this.byteBuffer = null;
return;
}
if (message == null)
// No complete message in buffer, but maybe more bytes to read from socket
@@ -452,18 +464,16 @@ public class Peer {
}
/* package */ void startPings() {
// Replacing initial null value allows pingCheck() to start sending pings.
// Replacing initial null value allows getPingTask() to start sending pings.
LOGGER.trace(() -> String.format("Enabling pings for peer %s", this));
this.lastPingSent = System.currentTimeMillis();
this.lastPingSent = NTP.getTime();
}
/* package */ ExecuteProduceConsume.Task getPingTask() {
/* package */ ExecuteProduceConsume.Task getPingTask(Long now) {
// Pings not enabled yet?
if (this.lastPingSent == null)
if (now == null || this.lastPingSent == null)
return null;
final long now = System.currentTimeMillis();
// Time to send another ping?
if (now < this.lastPingSent + PING_INTERVAL)
return null; // Not yet
@@ -474,14 +484,14 @@ public class Peer {
return () -> {
PingMessage pingMessage = new PingMessage();
Message message = this.getResponse(pingMessage);
final long after = System.currentTimeMillis();
if (message == null || message.getType() != MessageType.PING) {
LOGGER.debug(() -> String.format("Didn't receive reply from %s for PING ID %d", this, pingMessage.getId()));
this.disconnect("no ping received");
return;
}
this.setLastPing(after - now);
this.setLastPing(NTP.getTime() - now);
};
}

View File

@@ -5,6 +5,8 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.network.message.ProofMessage;
import com.google.common.primitives.Longs;
@@ -13,6 +15,7 @@ public class Proof extends Thread {
private static final int MIN_PROOF_ZEROS = 2;
private static final HashSet<Long> seenSalts = new HashSet<>();
private static final Logger LOGGER = LogManager.getLogger(Proof.class);
private Peer peer;
@@ -38,6 +41,7 @@ public class Proof extends Thread {
setName("Proof for peer " + this.peer);
// Do proof-of-work calculation to gain acceptance with remote end
final long startTime = LOGGER.isTraceEnabled() ? System.currentTimeMillis() : 0;
// Remote end knows this (approximately)
long timestamp = this.peer.getConnectionTimestamp();
@@ -64,7 +68,7 @@ public class Proof extends Thread {
long nonce;
for (nonce = 0; nonce < Long.MAX_VALUE; ++nonce) {
// Check whether we're shutting down every so often
if ((nonce & 0xff) == 0 && (peer.isStopping() || Thread.currentThread().isInterrupted()))
if ((nonce & 0xff) == 0 && (this.peer.isStopping() || Thread.currentThread().isInterrupted()))
// throw new InterruptedException("Interrupted during peer proof calculation");
return;
@@ -79,6 +83,8 @@ public class Proof extends Thread {
sha256.reset();
}
LOGGER.trace(() -> String.format("Proof for peer %s took %dms", this.peer, System.currentTimeMillis() - startTime));
ProofMessage proofMessage = new ProofMessage(timestamp, salt, nonce);
peer.sendMessage(proofMessage);
}

View File

@@ -1,5 +1,6 @@
package org.qortal.repository;
import java.math.BigDecimal;
import java.util.List;
import org.qortal.data.account.AccountBalanceData;
@@ -82,6 +83,12 @@ public interface AccountRepository {
*/
public void setMintedBlockCount(AccountData accountData) throws DataException;
/** Modifies account's minted block count only.
* <p>
* @return 2 if minted block count updated, 1 if block count set to delta, 0 if address not found.
*/
public int modifyMintedBlockCount(String address, int delta) throws DataException;
/** Delete account from repository. */
public void delete(String address) throws DataException;
@@ -105,6 +112,8 @@ public interface AccountRepository {
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException;
public void modifyAssetBalance(String address, long assetId, BigDecimal deltaBalance) throws DataException;
public void save(AccountBalanceData accountBalanceData) throws DataException;
public void delete(String address, long assetId) throws DataException;
@@ -155,6 +164,21 @@ public interface AccountRepository {
// Managing QORT from legacy QORA
/**
* Returns balance data for accounts with legacy QORA asset that are eligible
* for more block reward (block processing) or for block reward removal (block orphaning).
* <p>
* For block processing, accounts that have already received their final QORT reward for owning
* legacy QORA are omitted from the results. <tt>blockHeight</tt> should be <tt>null</tt>.
* <p>
* For block orphaning, accounts that did not receive a QORT reward at <tt>blockHeight</tt>
* are omitted from the results.
*
* @param blockHeight QORT reward must have be present at this height (for orphaning only)
* @throws DataException
*/
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException;
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException;
public void save(QortFromQoraData qortFromQoraData) throws DataException;

View File

@@ -4,9 +4,9 @@ import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.qortal.asset.Asset;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.MintingAccountData;
@@ -144,6 +144,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public void ensureAccount(AccountData accountData) throws DataException {
/*
* Why do we need to check/set the public_key?
* Is there something that sets an account's balance which also needs to set the public key?
byte[] publicKey = accountData.getPublicKey();
String sql = "SELECT public_key FROM Accounts WHERE account = ?";
@@ -168,6 +172,15 @@ public class HSQLDBAccountRepository implements AccountRepository {
} catch (SQLException e) {
throw new DataException("Unable to ensure minimal account in repository", e);
}
*/
String sql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
try {
this.repository.checkedExecuteUpdateCount(sql, accountData.getAddress());
} catch (SQLException e) {
throw new DataException("Unable to ensure minimal account in repository", e);
}
}
@Override
@@ -273,6 +286,18 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public int modifyMintedBlockCount(String address, int delta) throws DataException {
String sql = "INSERT INTO Accounts (account, blocks_minted) VALUES (?, ?) " +
"ON DUPLICATE KEY UPDATE blocks_minted = blocks_minted + ?";
try {
return this.repository.checkedExecuteUpdateCount(sql, address, delta, delta);
} catch (SQLException e) {
throw new DataException("Unable to modify account's minted block count in repository", e);
}
}
@Override
public void delete(String address) throws DataException {
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
@@ -470,6 +495,54 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void modifyAssetBalance(String address, long assetId, BigDecimal deltaBalance) throws DataException {
// If deltaBalance is zero then do nothing
if (deltaBalance.signum() == 0)
return;
// If deltaBalance is negative then we assume AccountBalances & parent Accounts rows exist
if (deltaBalance.signum() < 0) {
// Perform actual balance change
String sql = "UPDATE AccountBalances set balance = balance + ? WHERE account = ? AND asset_id = ?";
try {
this.repository.checkedExecuteUpdateCount(sql, deltaBalance, address, assetId);
} catch (SQLException e) {
throw new DataException("Unable to reduce account balance in repository", e);
}
// If balance is now zero, and there are no prior historic balances, then simply delete row for this address-assetId (typically during orphaning)
String deleteWhereSql = "account = ? AND asset_id = ? AND balance = 0 " + // covers "if balance now zero"
"AND (" +
"SELECT TRUE FROM HistoricAccountBalances " +
"WHERE account = ? AND asset_id = ? AND height < (SELECT height - 1 FROM NextBlockHeight) " +
"LIMIT 1" +
")";
try {
this.repository.delete("AccountBalances", deleteWhereSql, address, assetId, address, assetId);
} catch (SQLException e) {
throw new DataException("Unable to prune account balance in repository", e);
}
} else {
// We have to ensure parent row exists to satisfy foreign key constraint
try {
String sql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
this.repository.checkedExecuteUpdateCount(sql, address);
} catch (SQLException e) {
throw new DataException("Unable to ensure minimal account in repository", e);
}
// Perform actual balance change
String sql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE balance = balance + ?";
try {
this.repository.checkedExecuteUpdateCount(sql, address, assetId, deltaBalance, deltaBalance);
} catch (SQLException e) {
throw new DataException("Unable to increase account balance in repository", e);
}
}
}
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
// If balance is zero and there are no prior historic balance, then simply delete balances for this assetId (typically during orphaning)
@@ -490,13 +563,17 @@ public class HSQLDBAccountRepository implements AccountRepository {
throw new DataException("Unable to delete account balance from repository", e);
}
// I don't think we need to do this as Block.orphan() would do this for us?
/*
* I don't think we need to do this as Block.orphan() would do this for us?
try {
this.repository.delete("HistoricAccountBalances", "account = ? AND asset_id = ?", accountBalanceData.getAddress(), accountBalanceData.getAssetId());
} catch (SQLException e) {
throw new DataException("Unable to delete historic account balances from repository", e);
}
*/
return;
}
}
@@ -543,16 +620,17 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public RewardShareData getRewardShare(byte[] minterPublicKey, String recipient) throws DataException {
String sql = "SELECT reward_share_public_key, share_percent FROM RewardShares WHERE minter_public_key = ? AND recipient = ?";
String sql = "SELECT minter, reward_share_public_key, share_percent FROM RewardShares WHERE minter_public_key = ? AND recipient = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, minterPublicKey, recipient)) {
if (resultSet == null)
return null;
byte[] rewardSharePublicKey = resultSet.getBytes(1);
BigDecimal sharePercent = resultSet.getBigDecimal(2);
String minter = resultSet.getString(1);
byte[] rewardSharePublicKey = resultSet.getBytes(2);
BigDecimal sharePercent = resultSet.getBigDecimal(3);
return new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent);
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
} catch (SQLException e) {
throw new DataException("Unable to fetch reward-share info from repository", e);
}
@@ -560,17 +638,18 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public RewardShareData getRewardShare(byte[] rewardSharePublicKey) throws DataException {
String sql = "SELECT minter_public_key, recipient, share_percent FROM RewardShares WHERE reward_share_public_key = ?";
String sql = "SELECT minter_public_key, minter, recipient, share_percent FROM RewardShares WHERE reward_share_public_key = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, rewardSharePublicKey)) {
if (resultSet == null)
return null;
byte[] minterPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal sharePercent = resultSet.getBigDecimal(3);
String minter = resultSet.getString(2);
String recipient = resultSet.getString(3);
BigDecimal sharePercent = resultSet.getBigDecimal(4);
return new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent);
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
} catch (SQLException e) {
throw new DataException("Unable to fetch reward-share info from repository", e);
}
@@ -598,7 +677,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public List<RewardShareData> getRewardShares() throws DataException {
String sql = "SELECT minter_public_key, recipient, share_percent, reward_share_public_key FROM RewardShares";
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares";
List<RewardShareData> rewardShares = new ArrayList<>();
@@ -608,11 +687,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
do {
byte[] minterPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal sharePercent = resultSet.getBigDecimal(3);
byte[] rewardSharePublicKey = resultSet.getBytes(4);
String minter = resultSet.getString(2);
String recipient = resultSet.getString(3);
BigDecimal sharePercent = resultSet.getBigDecimal(4);
byte[] rewardSharePublicKey = resultSet.getBytes(5);
rewardShares.add(new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent));
rewardShares.add(new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent));
} while (resultSet.next());
return rewardShares;
@@ -625,7 +705,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
public List<RewardShareData> findRewardShares(List<String> minters, List<String> recipients, List<String> involvedAddresses,
Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT DISTINCT minter_public_key, recipient, share_percent, reward_share_public_key FROM RewardShares ");
sql.append("SELECT DISTINCT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares ");
List<Object> args = new ArrayList<>();
@@ -695,11 +775,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
do {
byte[] minterPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal sharePercent = resultSet.getBigDecimal(3);
byte[] rewardSharePublicKey = resultSet.getBytes(4);
String minter = resultSet.getString(2);
String recipient = resultSet.getString(3);
BigDecimal sharePercent = resultSet.getBigDecimal(4);
byte[] rewardSharePublicKey = resultSet.getBytes(5);
rewardShares.add(new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent));
rewardShares.add(new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent));
} while (resultSet.next());
return rewardShares;
@@ -724,7 +805,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public RewardShareData getRewardShareByIndex(int index) throws DataException {
String sql = "SELECT minter_public_key, recipient, share_percent, reward_share_public_key FROM RewardShares "
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares "
+ "ORDER BY reward_share_public_key ASC "
+ "OFFSET ? LIMIT 1";
@@ -733,11 +814,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
return null;
byte[] minterPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal sharePercent = resultSet.getBigDecimal(3);
byte[] rewardSharePublicKey = resultSet.getBytes(4);
String minter = resultSet.getString(2);
String recipient = resultSet.getString(3);
BigDecimal sharePercent = resultSet.getBigDecimal(4);
byte[] rewardSharePublicKey = resultSet.getBytes(5);
return new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent);
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
} catch (SQLException e) {
throw new DataException("Unable to fetch reward-share info from repository", e);
}
@@ -747,8 +829,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
public void save(RewardShareData rewardShareData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("RewardShares");
saveHelper.bind("minter_public_key", rewardShareData.getMinterPublicKey()).bind("recipient", rewardShareData.getRecipient())
.bind("reward_share_public_key", rewardShareData.getRewardSharePublicKey()).bind("share_percent", rewardShareData.getSharePercent());
saveHelper.bind("minter_public_key", rewardShareData.getMinterPublicKey()).bind("minter", rewardShareData.getMinter())
.bind("recipient", rewardShareData.getRecipient()).bind("reward_share_public_key", rewardShareData.getRewardSharePublicKey())
.bind("share_percent", rewardShareData.getSharePercent());
try {
saveHelper.execute(this.repository);
@@ -768,17 +851,19 @@ public class HSQLDBAccountRepository implements AccountRepository {
// Minting accounts used by BlockMinter
@Override
public List<MintingAccountData> getMintingAccounts() throws DataException {
List<MintingAccountData> mintingAccounts = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT minter_private_key FROM MintingAccounts")) {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT minter_private_key, minter_public_key FROM MintingAccounts")) {
if (resultSet == null)
return mintingAccounts;
do {
byte[] minterPrivateKey = resultSet.getBytes(1);
byte[] minterPublicKey = resultSet.getBytes(2);
mintingAccounts.add(new MintingAccountData(minterPrivateKey));
mintingAccounts.add(new MintingAccountData(minterPrivateKey, minterPublicKey));
} while (resultSet.next());
return mintingAccounts;
@@ -787,10 +872,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void save(MintingAccountData mintingAccountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("MintingAccounts");
saveHelper.bind("minter_private_key", mintingAccountData.getPrivateKey());
saveHelper.bind("minter_private_key", mintingAccountData.getPrivateKey())
.bind("minter_public_key", mintingAccountData.getPublicKey());
try {
saveHelper.execute(this.repository);
@@ -799,6 +886,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public int delete(byte[] minterPrivateKey) throws DataException {
try {
return this.repository.delete("MintingAccounts", "minter_private_key = ?", minterPrivateKey);
@@ -809,6 +897,42 @@ public class HSQLDBAccountRepository implements AccountRepository {
// Managing QORT from legacy QORA
@Override
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT account, balance from AccountBalances ");
sql.append("LEFT OUTER JOIN AccountQortFromQoraInfo USING (account) ");
sql.append("WHERE asset_id = ");
sql.append(Asset.LEGACY_QORA); // int is safe to use literally
sql.append(" AND (final_block_height IS NULL");
if (blockHeight != null) {
sql.append(" OR final_block_height >= ");
sql.append(blockHeight);
}
sql.append(")");
List<AccountBalanceData> accountBalances = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
if (resultSet == null)
return accountBalances;
do {
String address = resultSet.getString(1);
BigDecimal balance = resultSet.getBigDecimal(2).setScale(8);
accountBalances.add(new AccountBalanceData(address, Asset.LEGACY_QORA, balance));
} while (resultSet.next());
return accountBalances;
} catch (SQLException e) {
throw new DataException("Unable to fetch eligible legacy QORA holders from repository", e);
}
}
@Override
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException {
String sql = "SELECT final_qort_from_qora, final_block_height FROM AccountQortFromQoraInfo WHERE account = ?";
@@ -827,6 +951,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void save(QortFromQoraData qortFromQoraData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountQortFromQoraInfo");
@@ -841,6 +966,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public int deleteQortFromQoraInfo(String address) throws DataException {
try {
return this.repository.delete("AccountQortFromQoraInfo", "account = ?", address);

View File

@@ -201,7 +201,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
String subquerySql = "SELECT minter, COUNT(signature) FROM Blocks GROUP BY minter";
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT DISTINCT block_minter, n_blocks, minter_public_key, recipient FROM (");
sql.append("SELECT DISTINCT block_minter, n_blocks, minter_public_key, minter, recipient FROM (");
sql.append(subquerySql);
sql.append(") AS Minters (block_minter, n_blocks) LEFT OUTER JOIN RewardShares ON reward_share_public_key = block_minter ");
@@ -239,14 +239,17 @@ public class HSQLDBBlockRepository implements BlockRepository {
do {
byte[] blockMinterPublicKey = resultSet.getBytes(1);
int nBlocks = resultSet.getInt(2);
// May not be present if no reward-share:
byte[] mintingAccountPublicKey = resultSet.getBytes(3);
String recipientAccount = resultSet.getString(4);
String minterAccount = resultSet.getString(4);
String recipientAccount = resultSet.getString(5);
BlockMinterSummary blockMinterSummary;
if (recipientAccount == null)
blockMinterSummary = new BlockMinterSummary(blockMinterPublicKey, nBlocks);
else
blockMinterSummary = new BlockMinterSummary(blockMinterPublicKey, nBlocks, mintingAccountPublicKey, recipientAccount);
blockMinterSummary = new BlockMinterSummary(blockMinterPublicKey, nBlocks, mintingAccountPublicKey, minterAccount, recipientAccount);
summaries.add(blockMinterSummary);
} while (resultSet.next());
@@ -260,13 +263,13 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public List<BlockSummaryData> getBlockSummariesByMinter(byte[] minterPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(512);
sql.append("SELECT signature, height, minter, online_accounts_count FROM ");
sql.append("SELECT signature, height, Blocks.minter, online_accounts_count FROM ");
// List of minter account's public key and reward-share public keys with minter's public key
sql.append("(SELECT * FROM (VALUES (CAST(? AS QortalPublicKey))) UNION (SELECT reward_share_public_key FROM RewardShares WHERE minter_public_key = ?)) AS PublicKeys (public_key) ");
// Match Blocks signed with public key from above list
sql.append("JOIN Blocks ON minter = public_key ");
sql.append("JOIN Blocks ON Blocks.minter = public_key ");
sql.append("ORDER BY Blocks.height ");
if (reverse != null && reverse)

View File

@@ -928,6 +928,34 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX IF NOT EXISTS HistoricAccountBalancesHeightIndex ON HistoricAccountBalances (height)");
break;
case 66:
// Add CHECK constraint to account balances
stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)");
break;
case 67:
// Provide external function to convert private keys to public keys
stmt.execute("CREATE FUNCTION Ed25519_private_to_public_key (IN privateKey VARBINARY(32)) RETURNS VARBINARY(32) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PrivateToPublicKey'");
// Cache minting account public keys to save us recalculating them
stmt.execute("ALTER TABLE MintingAccounts ADD minter_public_key QortalPublicKey");
stmt.execute("UPDATE MintingAccounts SET minter_public_key = Ed25519_private_to_public_key(minter_private_key)");
stmt.execute("ALTER TABLE MintingAccounts ALTER COLUMN minter_public_key SET NOT NULL");
// Provide external function to convert public keys to addresses
stmt.execute("CREATE FUNCTION Ed25519_public_key_to_address (IN privateKey VARBINARY(32)) RETURNS VARCHAR(36) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PublicKeyToAddress'");
// Cache reward-share minting account's address
stmt.execute("ALTER TABLE RewardShares ADD minter QortalAddress BEFORE recipient");
stmt.execute("UPDATE RewardShares SET minter = Ed25519_public_key_to_address(minter_public_key)");
stmt.execute("ALTER TABLE RewardShares ALTER COLUMN minter SET NOT NULL");
break;
case 68:
// Slow down log fsync() calls from every 500ms to reduce I/O load
stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds
break;
default:
// nothing to do
return false;

View File

@@ -28,6 +28,8 @@ import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.crypto.Crypto;
import org.qortal.repository.ATRepository;
import org.qortal.repository.AccountRepository;
import org.qortal.repository.ArbitraryRepository;
@@ -57,6 +59,8 @@ public class HSQLDBRepository implements Repository {
protected List<String> sqlStatements;
protected long sessionId;
// Constructors
// NB: no visibility modifier so only callable from within same package
/* package */ HSQLDBRepository(Connection connection) throws DataException {
this.connection = connection;
@@ -84,6 +88,8 @@ public class HSQLDBRepository implements Repository {
assertEmptyTransaction("connection creation");
}
// Getters / setters
@Override
public ATRepository getATRepository() {
return new HSQLDBATRepository(this);
@@ -134,6 +140,18 @@ public class HSQLDBRepository implements Repository {
return new HSQLDBVotingRepository(this);
}
@Override
public boolean getDebug() {
return this.debugState;
}
@Override
public void setDebug(boolean debugState) {
this.debugState = debugState;
}
// Transaction COMMIT / ROLLBACK / savepoints
@Override
public void saveChanges() throws DataException {
try {
@@ -203,6 +221,8 @@ public class HSQLDBRepository implements Repository {
}
}
// Close / backup / rebuild / restore
@Override
public void close() throws DataException {
// Already closed? No need to do anything but maybe report double-call
@@ -257,16 +277,6 @@ public class HSQLDBRepository implements Repository {
}
}
@Override
public boolean getDebug() {
return this.debugState;
}
@Override
public void setDebug(boolean debugState) {
this.debugState = debugState;
}
@Override
public void backup(boolean quick) throws DataException {
if (!quick)
@@ -337,8 +347,8 @@ public class HSQLDBRepository implements Repository {
Path oldRepoFilePath = oldRepoPath.getFileName();
// Try to open backup. We need to remove "create=true" and insert "backup" dir before final filename.
String backupUrlTemplate = "jdbc:hsqldb:file:%s/backup/%s;create=false;hsqldb.full_log_replay=true";
return String.format(backupUrlTemplate, oldRepoDirPath.toString(), oldRepoFilePath.toString());
String backupUrlTemplate = "jdbc:hsqldb:file:%s%sbackup%s%s;create=false;hsqldb.full_log_replay=true";
return String.format(backupUrlTemplate, oldRepoDirPath.toString(), File.separator, File.separator, oldRepoFilePath.toString());
}
/* package */ static void attemptRecovery(String connectionUrl) throws DataException {
@@ -361,8 +371,8 @@ public class HSQLDBRepository implements Repository {
.forEach(File::delete);
try (Statement stmt = connection.createStatement()) {
// Now "backup" the backup back to original repository location (the parent)
// NOTE: trailing / is OK because HSQLDB checks for both / and O/S-specific separator
// Now "backup" the backup back to original repository location (the parent).
// NOTE: trailing / is OK because HSQLDB checks for both / and O/S-specific separator.
// textdb.allow_full_path connection property is required to be able to use '..'
stmt.execute("BACKUP DATABASE TO '../' BLOCKING AS FILES");
} catch (SQLException e) {
@@ -386,6 +396,8 @@ public class HSQLDBRepository implements Repository {
}
}
// SQL statements, etc.
/**
* Returns prepared statement using passed SQL, logging query if necessary.
*/
@@ -399,19 +411,6 @@ public class HSQLDBRepository implements Repository {
return this.connection.prepareStatement(sql);
}
/**
* Logs this transaction's SQL statements, if enabled.
*/
public void logStatements() {
if (this.sqlStatements == null)
return;
LOGGER.info(String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
for (String sql : this.sqlStatements)
LOGGER.info(sql);
}
/**
* Execute SQL and return ResultSet with but added checking.
* <p>
@@ -429,15 +428,18 @@ public class HSQLDBRepository implements Repository {
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
preparedStatement.closeOnCompletion();
long beforeQuery = System.currentTimeMillis();
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects);
long queryTime = System.currentTimeMillis() - beforeQuery;
if (this.slowQueryThreshold != null && queryTime > this.slowQueryThreshold) {
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
logStatements();
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
logStatements();
}
}
return resultSet;
@@ -500,16 +502,19 @@ public class HSQLDBRepository implements Repository {
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
prepareExecute(preparedStatement, objects);
long beforeQuery = System.currentTimeMillis();
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
if (preparedStatement.execute())
throw new SQLException("Database produced results, not row count");
long queryTime = System.currentTimeMillis() - beforeQuery;
if (this.slowQueryThreshold != null && queryTime > this.slowQueryThreshold) {
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
logStatements();
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
logStatements();
}
}
int rowCount = preparedStatement.getUpdateCount();
@@ -670,6 +675,21 @@ public class HSQLDBRepository implements Repository {
stringBuilder.append(") ");
}
// Debugging
/**
* Logs this transaction's SQL statements, if enabled.
*/
public void logStatements() {
if (this.sqlStatements == null)
return;
LOGGER.info(String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
for (String sql : this.sqlStatements)
LOGGER.info(sql);
}
/** Logs other HSQLDB sessions then re-throws passed exception */
public SQLException examineException(SQLException e) throws SQLException {
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
@@ -726,6 +746,22 @@ public class HSQLDBRepository implements Repository {
}
}
// Utility methods
public static byte[] ed25519PrivateToPublicKey(byte[] privateKey) {
if (privateKey == null)
return null;
return PrivateKeyAccount.toPublicKey(privateKey);
}
public static String ed25519PublicKeyToAddress(byte[] publicKey) {
if (publicKey == null)
return null;
return Crypto.toAddress(publicKey);
}
/** Converts milliseconds from epoch to OffsetDateTime needed for TIMESTAMP WITH TIME ZONE columns. */
/* package */ static OffsetDateTime toOffsetDateTime(Long timestamp) {
if (timestamp == null)

View File

@@ -31,9 +31,6 @@ public class Settings {
private static final int MAINNET_API_PORT = 12391;
private static final int TESTNET_API_PORT = 62391;
private static final int MAINNET_UI_PORT = 12390;
private static final int TESTNET_UI_PORT = 62390;
private static final Logger LOGGER = LogManager.getLogger(Settings.class);
private static final String SETTINGS_FILENAME = "settings.json";
@@ -43,14 +40,17 @@ public class Settings {
// Settings, and other config files
private String userPath;
// Common to all networking (UI/API/P2P)
// Common to all networking (API/P2P)
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
// Node management UI
private boolean uiEnabled = true;
private Integer uiPort;
private String[] uiWhitelist = new String[] {
"::1", "127.0.0.1"
// UI servers
private int uiPort = 12388;
private String[] uiLocalServers = new String[] {
"localhost", "172.24.1.1", "qor.tal"
};
private String[] uiRemoteServers = new String[] {
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org"
};
// API-related
@@ -71,17 +71,21 @@ public class Settings {
private int maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds
/** Whether we check, fetch and install auto-updates */
private boolean autoUpdateEnabled = true;
/** Whether to show a notification when we backup repository. */
private boolean showBackupNotification = false;
// Peer-to-peer related
private boolean isTestNet = false;
/** Port number for inbound peer-to-peer connections. */
private Integer listenPort;
/** Minimum number of peers to allow block minting / synchronization. */
private int minBlockchainPeers = 5;
private int minBlockchainPeers = 10;
/** Target number of outbound connections to peers we should make. */
private int minOutboundPeers = 20;
private int minOutboundPeers = 25;
/** Maximum number of peer connections we allow. */
private int maxPeers = 50;
/** Maximum number of threads for network engine. */
private int maxNetworkThreadPoolSize = 20;
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
@@ -113,7 +117,7 @@ public class Settings {
"3.cn.pool.ntp.org"
};
/** Additional offset added to values returned by NTP.getTime() */
private long testNtpOffset = 0;
private Long testNtpOffset = null;
// Constructors
@@ -244,19 +248,16 @@ public class Settings {
return this.userPath;
}
public boolean isUiEnabled() {
return this.uiEnabled;
public int getUiServerPort() {
return this.uiPort;
}
public int getUiPort() {
if (this.uiPort != null)
return this.uiPort;
return this.isTestNet ? TESTNET_UI_PORT : MAINNET_UI_PORT;
public String[] getLocalUiServers() {
return this.uiLocalServers;
}
public String[] getUiWhitelist() {
return this.uiWhitelist;
public String[] getRemoteUiServers() {
return this.uiRemoteServers;
}
public boolean isApiEnabled() {
@@ -334,6 +335,10 @@ public class Settings {
return this.maxPeers;
}
public int getMaxNetworkThreadPoolSize() {
return this.maxNetworkThreadPoolSize;
}
public String getBlockchainConfig() {
return this.blockchainConfig;
}
@@ -362,8 +367,12 @@ public class Settings {
return this.ntpServers;
}
public long getTestNtpOffset() {
public Long getTestNtpOffset() {
return this.testNtpOffset;
}
public boolean getShowBackupNotification() {
return this.showBackupNotification;
}
}

View File

@@ -195,7 +195,9 @@ public class RewardShareTransaction extends Transaction {
this.repository.getAccountRepository().delete(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient());
} else {
// Save reward-share info
rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient(), rewardShareTransactionData.getRewardSharePublicKey(), rewardShareTransactionData.getSharePercent());
rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), mintingAccount.getAddress(),
rewardShareTransactionData.getRecipient(), rewardShareTransactionData.getRewardSharePublicKey(),
rewardShareTransactionData.getSharePercent());
this.repository.getAccountRepository().save(rewardShareData);
}
}
@@ -217,8 +219,9 @@ public class RewardShareTransaction extends Transaction {
if (rewardShareTransactionData.getPreviousSharePercent() != null) {
// Revert previous sharing arrangement
RewardShareData rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient(),
rewardShareTransactionData.getRewardSharePublicKey(), rewardShareTransactionData.getPreviousSharePercent());
RewardShareData rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), mintingAccount.getAddress(),
rewardShareTransactionData.getRecipient(), rewardShareTransactionData.getRewardSharePublicKey(),
rewardShareTransactionData.getPreviousSharePercent());
this.repository.getAccountRepository().save(rewardShareData);
} else {

View File

@@ -241,6 +241,7 @@ public abstract class Transaction {
ASSET_NOT_SPENDABLE(89),
ACCOUNT_CANNOT_REWARD_SHARE(90),
SELF_SHARE_EXISTS(91),
ACCOUNT_ALREADY_EXISTS(92),
NOT_YET_RELEASED(1000);
public final int value;

View File

@@ -83,6 +83,10 @@ public class TransferPrivsTransaction extends Transaction {
if (!Crypto.isValidAddress(this.transferPrivsTransactionData.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
// Check recipient is new account
if (this.repository.getAccountRepository().accountExists(this.transferPrivsTransactionData.getRecipient()))
return ValidationResult.ACCOUNT_ALREADY_EXISTS;
return ValidationResult.OK;
}
@@ -183,8 +187,11 @@ public class TransferPrivsTransaction extends Transaction {
accountRepository.setFlags(senderData);
// Restore recipient's flags
recipientData.setFlags(this.transferPrivsTransactionData.getPreviousRecipientFlags());
accountRepository.setFlags(recipientData);
Integer previousRecipientFlags = this.transferPrivsTransactionData.getPreviousRecipientFlags();
if (previousRecipientFlags != null) {
recipientData.setFlags(previousRecipientFlags);
accountRepository.setFlags(recipientData);
}
// Clean values in transaction data
this.transferPrivsTransactionData.setPreviousSenderFlags(null);

View File

@@ -1,46 +0,0 @@
package org.qortal.ui;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.ResourceService;
import org.eclipse.jetty.util.URIUtil;
/**
* Replace ResourceService that delivers content as "attachments", typically forcing download instead of rendering.
* <p>
* Sets <tt>Content-Type</tt> header to <tt>application/octet-stream</tt><br>
* Sets <tt>Content-Disposition</tt> header to <tt>attachment; filename="<i>basename</i>"</tt><br>
* where <i>basename</i> is that last component of requested URI path.
* <p>
* Example usage:<br>
* <br>
* <tt>... = new ServletHolder("servlet-name", new DefaultServlet(new DownloadResourceService()));</tt>
*/
public class DownloadResourceService extends ResourceService {
@Override
protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, final HttpContent content, Enumeration<String> reqRanges) throws IOException {
final boolean _pathInfoOnly = super.isPathInfoOnly();
String servletPath = _pathInfoOnly ? "/" : request.getServletPath();
String pathInfo = request.getPathInfo();
String pathInContext = URIUtil.addPaths(servletPath,pathInfo);
// Find basename of requested content
final int slashIndex = pathInContext.lastIndexOf(URIUtil.SLASH);
if (slashIndex != -1)
pathInContext = pathInContext.substring(slashIndex + 1);
// Add appropriate headers
response.setHeader(HttpHeader.CONTENT_TYPE.asString(), "application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" + pathInContext + "\"");
return super.sendData(request, response, include, content, reqRanges);
}
}

View File

@@ -1,101 +0,0 @@
package org.qortal.ui;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.InetAccessHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.qortal.settings.Settings;
public class UiService {
public static final String DOWNLOADS_RESOURCE_PATH = "node-ui-downloads";
private static UiService instance;
private Server server;
private UiService() {
}
public static UiService getInstance() {
if (instance == null)
instance = new UiService();
return instance;
}
public void start() {
try {
// Create node management UI server
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getUiPort());
this.server = new Server(endpoint);
// IP address based access control
InetAccessHandler accessHandler = new InetAccessHandler();
for (String pattern : Settings.getInstance().getUiWhitelist()) {
accessHandler.include(pattern);
}
this.server.setHandler(accessHandler);
// URL rewriting
RewriteHandler rewriteHandler = new RewriteHandler();
accessHandler.setHandler(rewriteHandler);
// Context
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
rewriteHandler.setHandler(context);
// Cross-origin resource sharing
FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
context.addFilter(corsFilterHolder, "/*", null);
ClassLoader loader = this.getClass().getClassLoader();
// Node management UI download servlet
ServletHolder uiDownloadServlet = new ServletHolder("node-ui-download", new DefaultServlet(new DownloadResourceService()));
uiDownloadServlet.setInitParameter("resourceBase", loader.getResource(DOWNLOADS_RESOURCE_PATH + "/").toString());
uiDownloadServlet.setInitParameter("dirAllowed", "true");
uiDownloadServlet.setInitParameter("pathInfoOnly", "true");
context.addServlet(uiDownloadServlet, "/downloads/*");
// Node management UI static content servlet
ServletHolder uiServlet = new ServletHolder("node-management-ui", DefaultServlet.class);
uiServlet.setInitParameter("resourceBase", loader.getResource("node-management-ui/").toString());
uiServlet.setInitParameter("dirAllowed", "true");
uiServlet.setInitParameter("pathInfoOnly", "true");
context.addServlet(uiServlet, "/*");
rewriteHandler.addRule(new RedirectPatternRule("", "/index.html")); // node management UI start page
// Start server
this.server.start();
} catch (Exception e) {
// Failed to start
throw new RuntimeException("Failed to start node management UI", e);
}
}
public void stop() {
try {
// Stop server
this.server.stop();
} catch (Exception e) {
// Failed to stop
}
this.server = null;
}
}

View File

@@ -5,13 +5,30 @@ import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public abstract class ExecuteProduceConsume implements Runnable {
@XmlAccessorType(XmlAccessType.FIELD)
public static class StatsSnapshot {
public int activeThreadCount = 0;
public int greatestActiveThreadCount = 0;
public int consumerCount = 0;
public int tasksProduced = 0;
public int tasksConsumed = 0;
public int spawnFailures = 0;
public StatsSnapshot() {
}
}
private final String className;
private final Logger logger;
private final boolean isLoggerTraceEnabled;
private ExecutorService executor;
@@ -24,12 +41,14 @@ public abstract class ExecuteProduceConsume implements Runnable {
private volatile int consumerCount = 0;
private volatile int tasksProduced = 0;
private volatile int tasksConsumed = 0;
private volatile int spawnFailures = 0;
private volatile boolean hasThreadPending = false;
public ExecuteProduceConsume(ExecutorService executor) {
this.className = this.getClass().getSimpleName();
this.logger = LogManager.getLogger(this.getClass());
this.isLoggerTraceEnabled = this.logger.isTraceEnabled();
this.executor = executor;
}
@@ -51,28 +70,19 @@ public abstract class ExecuteProduceConsume implements Runnable {
return this.executor.awaitTermination(timeout, TimeUnit.MILLISECONDS);
}
public int getActiveThreadCount() {
synchronized (this) {
return this.activeThreadCount;
}
}
public StatsSnapshot getStatsSnapshot() {
StatsSnapshot snapshot = new StatsSnapshot();
public int getGreatestActiveThreadCount() {
synchronized (this) {
return this.greatestActiveThreadCount;
snapshot.activeThreadCount = this.activeThreadCount;
snapshot.greatestActiveThreadCount = this.greatestActiveThreadCount;
snapshot.consumerCount = this.consumerCount;
snapshot.tasksProduced = this.tasksProduced;
snapshot.tasksConsumed = this.tasksConsumed;
snapshot.spawnFailures = this.spawnFailures;
}
}
public int getTasksProduced() {
synchronized (this) {
return this.tasksProduced;
}
}
public int getTasksConsumed() {
synchronized (this) {
return this.tasksConsumed;
}
return snapshot;
}
/**
@@ -91,7 +101,8 @@ public abstract class ExecuteProduceConsume implements Runnable {
@Override
public void run() {
Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
if (this.isLoggerTraceEnabled)
Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
boolean wasThreadPending;
synchronized (this) {
@@ -126,10 +137,9 @@ public abstract class ExecuteProduceConsume implements Runnable {
this.logger.trace(() -> String.format("[%d] producing, activeThreadCount: %d, consumerCount: %d, canBlock is %b...",
Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount, lambdaCanIdle));
final long now = System.currentTimeMillis();
final long beforeProduce = isLoggerTraceEnabled ? System.currentTimeMillis() : 0;
task = produceTask(canBlock);
final long delay = System.currentTimeMillis() - now;
this.logger.trace(() -> String.format("[%d] producing took %dms", Thread.currentThread().getId(), delay));
this.logger.trace(() -> String.format("[%d] producing took %dms", Thread.currentThread().getId(), System.currentTimeMillis() - beforeProduce));
}
if (task == null)
@@ -167,6 +177,7 @@ public abstract class ExecuteProduceConsume implements Runnable {
try {
this.executor.execute(this); // Same object, different thread
} catch (RejectedExecutionException e) {
++this.spawnFailures;
this.hasThreadPending = false;
this.logger.trace(() -> String.format("[%d] failed to spawn another thread", Thread.currentThread().getId()));
}
@@ -193,7 +204,8 @@ public abstract class ExecuteProduceConsume implements Runnable {
} catch (InterruptedException e) {
// We're in shutdown situation so exit
} finally {
Thread.currentThread().setName(this.className + "-dormant");
if (this.isLoggerTraceEnabled)
Thread.currentThread().setName(this.className);
}
}

View File

@@ -11,6 +11,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.NtpV3Packet;
@@ -18,7 +19,6 @@ import org.apache.commons.net.ntp.TimeInfo;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.settings.Settings;
public class NTP implements Runnable {
@@ -53,15 +53,10 @@ public class NTP implements Runnable {
this.remote = remote;
}
public boolean doPoll(NTPUDPClient client) {
public boolean doPoll(NTPUDPClient client, final long now) {
Thread.currentThread().setName(String.format("NTP: %s", this.remote));
try {
final long now = System.currentTimeMillis();
if (now < this.nextPoll)
return false;
boolean isUpdated = false;
try {
TimeInfo timeInfo = client.getTime(InetAddress.getByName(remote));
@@ -110,26 +105,26 @@ public class NTP implements Runnable {
}
private final NTPUDPClient client;
private List<NTPServer> ntpServers = new ArrayList<>();
private final List<NTPServer> ntpServers = new ArrayList<>();
private final ExecutorService serverExecutor;
private NTP() {
private NTP(String[] serverNames) {
client = new NTPUDPClient();
client.setDefaultTimeout(2000);
for (String serverName : Settings.getInstance().getNtpServers())
for (String serverName : serverNames)
ntpServers.add(new NTPServer(serverName));
serverExecutor = Executors.newCachedThreadPool();
}
public static synchronized void start() {
public static synchronized void start(String[] serverNames) {
if (isStarted)
return;
isStarted = true;
instanceExecutor = Executors.newSingleThreadExecutor();
instance = new NTP();
instance = new NTP(serverNames);
instanceExecutor.execute(instance);
}
@@ -137,9 +132,9 @@ public class NTP implements Runnable {
instanceExecutor.shutdownNow();
}
public static synchronized void testMode() {
// Fix offset to match system time
NTP.offset = 0L;
public static synchronized void setFixedOffset(Long offset) {
// Fix offset, e.g. for testing
NTP.offset = offset;
}
/**
@@ -151,7 +146,7 @@ public class NTP implements Runnable {
if (NTP.offset == null)
return null;
return System.currentTimeMillis() + NTP.offset + Settings.getInstance().getTestNtpOffset();
return System.currentTimeMillis() + NTP.offset;
}
public void run() {
@@ -161,103 +156,120 @@ public class NTP implements Runnable {
while (!isStopping) {
Thread.sleep(1000);
CompletionService<Boolean> ecs = new ExecutorCompletionService<>(serverExecutor);
for (NTPServer server : ntpServers)
ecs.submit(() -> server.doPoll(client));
boolean haveUpdates = pollServers();
if (!haveUpdates)
continue;
boolean hasUpdate = false;
for (int i = 0; i < ntpServers.size(); ++i) {
if (isStopping)
return;
try {
hasUpdate = ecs.take().get() || hasUpdate;
} catch (ExecutionException e) {
// skip
}
}
if (hasUpdate) {
double s0 = 0;
double s1 = 0;
double s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null) {
server.usage = ' ';
continue;
}
server.usage = '+';
double value = server.offset * (double) server.stratum;
s0 += 1;
s1 += value;
s2 += value * value;
}
if (s0 < ntpServers.size() / 3 + 1) {
LOGGER.debug(String.format("Not enough replies (%d) to calculate network time", s0));
} else {
double thresholdStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
double mean = s1 / s0;
// Now only consider offsets within 1 stddev?
s0 = 0;
s1 = 0;
s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null || server.reach == 0)
continue;
if (Math.abs(server.offset * (double)server.stratum - mean) > thresholdStddev)
continue;
server.usage = '*';
s0 += 1;
s1 += server.offset;
s2 += server.offset * server.offset;
}
if (s0 <= 1) {
LOGGER.debug(String.format("Not enough useful values (%d) to calculate network time. (stddev: %7.4f)", s0, thresholdStddev));
} else {
double filteredMean = s1 / s0;
double filteredStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
LOGGER.trace(String.format("Threshold stddev: %7.3f, mean: %7.3f, stddev: %7.3f, nValues: %.0f / %d",
thresholdStddev, filteredMean, filteredStddev, s0, ntpServers.size()));
NTP.offset = (long) filteredMean;
LOGGER.debug(String.format("New NTP offset: %d", NTP.offset));
}
}
if (LOGGER.getLevel().isMoreSpecificThan(Level.TRACE)) {
LOGGER.trace(String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
));
for (NTPServer server : ntpServers)
LOGGER.trace(String.format("%c%16.16s %16.16s %2s %c %4s %4d %3o %7s %7s %7s",
server.usage,
server.remote,
formatNull("%s", server.refId, ""),
formatNull("%2d", server.stratum, ""),
server.type,
formatNull("%4d", server.getWhen(), "-"),
server.poll,
server.reach,
formatNull("%5dms", server.delay, ""),
formatNull("% 5.0fms", server.offset, ""),
formatNull("%5.2fms", server.jitter, "")
));
}
}
calculateOffset();
}
} catch (InterruptedException e) {
// Exit
// Interrupted - time to exit
return;
}
}
private boolean pollServers() throws InterruptedException {
final long now = System.currentTimeMillis();
List<NTPServer> pendingServers = ntpServers.stream().filter(ntpServer -> now >= ntpServer.nextPoll).collect(Collectors.toList());
CompletionService<Boolean> ecs = new ExecutorCompletionService<>(serverExecutor);
for (NTPServer server : pendingServers)
ecs.submit(() -> server.doPoll(client, now));
boolean haveUpdate = false;
for (int i = 0; i < pendingServers.size(); ++i) {
if (isStopping)
return false;
try {
haveUpdate = ecs.take().get() || haveUpdate;
} catch (ExecutionException e) {
// skip
}
}
return haveUpdate;
}
private void calculateOffset() {
double s0 = 0;
double s1 = 0;
double s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null) {
server.usage = ' ';
continue;
}
server.usage = '+';
double value = server.offset * (double) server.stratum;
s0 += 1;
s1 += value;
s2 += value * value;
}
if (s0 < ntpServers.size() / 3 + 1) {
final double numberReplies = s0;
LOGGER.debug(() -> String.format("Not enough replies (%d) to calculate network time", numberReplies));
} else {
double thresholdStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
double mean = s1 / s0;
// Now only consider offsets within 1 stddev?
s0 = 0;
s1 = 0;
s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null || server.reach == 0)
continue;
if (Math.abs(server.offset * (double)server.stratum - mean) > thresholdStddev)
continue;
server.usage = '*';
s0 += 1;
s1 += server.offset;
s2 += server.offset * server.offset;
}
final double numberValues = s0;
if (s0 <= 1) {
LOGGER.debug(() -> String.format("Not enough useful values (%d) to calculate network time. (stddev: %7.4f)", numberValues, thresholdStddev));
} else {
double filteredMean = s1 / s0;
double filteredStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
LOGGER.trace(() -> String.format("Threshold stddev: %7.3f, mean: %7.3f, stddev: %7.3f, nValues: %.0f / %d",
thresholdStddev, filteredMean, filteredStddev, numberValues, ntpServers.size()));
NTP.offset = (long) filteredMean;
LOGGER.debug(() -> String.format("New NTP offset: %d", NTP.offset));
}
}
if (LOGGER.getLevel().isLessSpecificThan(Level.TRACE)) {
LOGGER.trace(() -> String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
));
for (NTPServer server : ntpServers)
LOGGER.trace(() -> String.format("%c%16.16s %16.16s %2s %c %4s %4d %3o %7s %7s %7s",
server.usage,
server.remote,
formatNull("%s", server.refId, ""),
formatNull("%2d", server.stratum, ""),
server.type,
formatNull("%4d", server.getWhen(), "-"),
server.poll,
server.reach,
formatNull("%5dms", server.delay, ""),
formatNull("% 5.0fms", server.offset, ""),
formatNull("%5.2fms", server.jitter, "")
));
}
}

View File

@@ -0,0 +1,29 @@
package org.qortal.utils;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
public class RandomizeList {
private static final Random random = new Random();
public static <T> List<T> randomize(List<T> inputList) {
List<T> outputList = new ArrayList<T>();
Iterator<T> inputIterator = inputList.iterator();
while (inputIterator.hasNext()) {
T element = inputIterator.next();
if (outputList.isEmpty()) {
outputList.add(element);
} else {
int outputIndex = random.nextInt(outputList.size() + 1);
outputList.add(outputIndex, element);
}
}
return outputList;
}
}

View File

@@ -7,6 +7,7 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
@@ -28,7 +29,14 @@ public class Serialization {
// (At least until the BigDecimal XmlAdapter works - see data/package-info.java)
byte[] amountBytes = amount.setScale(8).unscaledValue().toByteArray();
byte[] output = new byte[length];
// To retain sign of 'amount', we might need to explicitly fill 'output' with leading 1s
if (amount.signum() == -1)
// Negative values: fill output with 1s
Arrays.fill(output, (byte) 0xff);
System.arraycopy(amountBytes, 0, output, length - amountBytes.length, amountBytes.length);
return output;
}

View File

@@ -9,7 +9,7 @@
"defaultGroupId": 0,
"oneNamePerAccount": true,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"maxRewardSharesPerMintingAccount": 6,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 2592000000,
"onlineAccountSignaturesMaxLifetime": 3196800000,
@@ -57,21 +57,22 @@
},
"genesisInfo": {
"version": 4,
"timestamp": "1580820000000",
"timestamp": "1583870000000",
"transactions": [
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 0, "isDivisible": true, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "ACCOUNT_FLAGS", "target": "QY82MasqEH6ChwXaETH4piMtE8Pk4NBWD3", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QdAESUvjxT764zNnYyo53DVbu7nusmjuee", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QcPro2T97Q8cAfcVM4Pn4fv71Za4T6oeFD", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QbJqEntoBFps7XECQkTDFzXNCdz9R2qmkB", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "Qc5sZS1Vb1ujj8qvL5uXV5y5yQPq6pw2GC", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QceNmCiZxxLdvL85huifVcnk64udcJ47Jr", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "Qd453ewoyESrEgUab6dTFe2pufWkD94Tsm", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QfjoMGib4trpZHzxUSMdmtiRnsrLNf74zp", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "Qi3N6fNRrs15EHmkxYyWHyh4z3Dp2rVU2i", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QicRwDhfk8M2CGNvpMEmYzQEjESvF7WrFY", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QLwMaXmDDUvh7aN5MdpY28rqTKE8U1Cepc", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QP3J3GHgjqP69neTAprpYe4co33eKQiQpS", "andMask": -1, "orMask": 1, "xorMask": 0 },
@@ -84,10 +85,29 @@
{ "type": "ACCOUNT_FLAGS", "target": "QTx98gU8ErigXkViWkvRH5JfaMpk8b3bHe", "andMask": -1, "orMask": 0, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QbmBdAbvmXqCya8bia8WaD5izumKkC9BrY", "andMask": -1, "orMask": 0, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QNuvYvBRUQgNj8u7gbLqBruDjKL1qzemND", "andMask": -1, "orMask": 0, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QSJ5cDLWivGcn9ym21azufBfiqeuGH1maq", "andMask": -1, "orMask": 0, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QhPXuFFRa9s91Q2qYKpSN5LVCUTqYkgRLz", "andMask": -1, "orMask": 0, "xorMask": 0 },
{ "type": "ACCOUNT_LEVEL", "target": "QPFM4xX2826MuBEhMtdReW1QR3vRYrQff3", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QVhboSLD1VmX2YvAnfAXkbzvsmXkDJZTNR", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QagyUZdmnKJA9LyEqcxJFFf2ehmcqVZsKb", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QcCYuXos5xBXXHbRg1RTfSdxiZEkGa3N2P", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "Qg3TKLhPvn7bKVrT9x37wJiJ7YZ4jBuqQW", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QiQUssukhoo1ft4G9Mxa8JpViqFW4PdBjJ", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QTVLRwoopfg7qSG9CMfCPJz3UydnT3jDxD", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QPLoqpwAoytvpQKwvJ6GRsaRcVZ3xnYgVB", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QRPhK2FwijTaMQ4PeJ9gFK1vvGBaYaht2P", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QRk5TG57SQGLkybXUqxBnobADTFGj9GR3Z", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QTrosCDj7oCdJc8K7sJZW6ssHU3StVoY7s", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QVgX5FVNvAvESmeVD67vz3xE7BQTQARLLM", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QVxGkDgXt4nHj4MAd1afV9AxT1XCUVLGja", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QXHmtFXzf4D7PEu73NfBm3sZyeuGrm3QC5", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "Qata5oApMShnD4F1kcgSJMTiYsxTPSFW4F", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QeoFPT4xPxLp2P42yKVo22wx2V8kydfNmr", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "Qi7Lbvz1UdWgzgbKzfpRisLsdLR4smVoVh", "level": 3 },
{ "type": "REWARD_SHARE", "minterPublicKey": "HFDmuc4HAAoVs9Siea3MugjBHasbotgVz2gsRDuLAAcB", "recipient": "QY82MasqEH6ChwXaETH4piMtE8Pk4NBWD3", "rewardSharePublicKey": "F35TbQXmgzz32cALj29jxzpdYSUKQvssqThLsZSabSXx", "sharePercent": 0 },
{ "type": "CREATE_GROUP", "creatorPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "owner": "QbmBdAbvmXqCya8bia8WaD5izumKkC9BrY", "groupName": "dev-group", "description": "developer group", "isOpen": true, "approvalThreshold": "ONE", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
@@ -95,381 +115,406 @@
{ "type": "JOIN_GROUP", "joinerPublicKey": "Gqcfa6YGXnKTr4F3QMpDQkjxxLGBmsq83wXcYeZvES7i", "groupId": 1 },
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QTx98gU8ErigXkViWkvRH5JfaMpk8b3bHe" },
{ "type": "JOIN_GROUP", "joinerPublicKey": "CrHdj5wKFtu7961zYvCxW76K1hHPSNpo43XP1hTWryJR", "groupId": 1 },
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QNuvYvBRUQgNj8u7gbLqBruDjKL1qzemND" },
{ "type": "JOIN_GROUP", "joinerPublicKey": "2ZnSnZcpvMePw23f3AefANbRQHs7KjDyAyEzXc9dBMdn", "groupId": 1 },
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QSJ5cDLWivGcn9ym21azufBfiqeuGH1maq" },
{ "type": "JOIN_GROUP", "joinerPublicKey": "25jxnXz3xF3tK2WJHpUHwGx7DXR6yfjAHF1hFX5ice6H", "groupId": 1 },
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QhPXuFFRa9s91Q2qYKpSN5LVCUTqYkgRLz" },
{ "type": "UPDATE_GROUP", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "newOwner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT20", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
{ "type": "REGISTER_NAME", "registrantPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "owner": "QbmBdAbvmXqCya8bia8WaD5izumKkC9BrY", "name": "ciyam", "data": "{}" },
{ "type": "UPDATE_GROUP", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "newOwner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
{ "type": "ACCOUNT_LEVEL", "target": "QVLuvt9krmxXwQPAeAhxzhuMF5i8F4aNs8", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QgECFJiiri2dDN4zA32URvbdDid2cFrJwM", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QQa3MTgdnru5B7wSqPcq7qXcZcpbDQ7oyE", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QXan6JJ1WuRi7GigvFDjtTzJY1rfYEqEqv", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QQa3MTgdnru5B7wSqPcq7qXcZcpbDQ7oyE", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QQoHo1x4hvZcSFbj2uQAXQhq2ZXBUgPeNj", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QQEZEGWt3sAPwEWYD2RQ6tMwnpkayG81dY", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QUvoLFfkuVuRe1KGMLQS4nUHry6CBTuTYz", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QTEE4ZJXv68ke4841HWjTLAAU8mfccxwbE", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QQoHo1x4hvZcSFbj2uQAXQhq2ZXBUgPeNj", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QfmM8dgfikTB2FYVuJ9owzQXVm8wP7T4QT", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QPUMyJ59kkrp75tDzDPxSyw1GWCrbC2cS2", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QQEZEGWt3sAPwEWYD2RQ6tMwnpkayG81dY", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "Qc9dZchoYfc1eRJhSLXR9rxSHcqNB47Dex", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QTMTFswUU83XVmk6T4Gez7qUJCccbAad7S", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QPcTWoAhYWmwjmWbQAS8muisrQVaLJMbg7", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QWpJxitn53ovwknQfrCaqivvoUNuPrX2sb", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QLpLY9o3X21q7q4L6u5JdDuMskYA838iYE", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QXv4WiqgryFPi8BgX7RU7gqtAgrVutmU4S", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QSkiGy3v3MwvGy3aACuyLDv3Xy2AWAYfPS", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QgpAW6uqwNR58gNYWRCVXNLm4F5TuckAw4", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QLpLY9o3X21q7q4L6u5JdDuMskYA838iYE", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QguNJ4evpN9i76dYHysTzLiKoWKFhE4B4U", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QWpJxitn53ovwknQfrCaqivvoUNuPrX2sb", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QP8xG56L8b28h1mguSk9LuzNhxbHgAoL9b", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QfjL32jLsxtumbfx6ufmfCFCBccVCQFkrh", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QWLpsGYrkF2cy3tH6DCxso7kXZpZJvv13e", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QbwgBD6LWdk1hZsb8EwdVVmDZdvpxMzyGT", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "Qbcy4uyMkQF2JXYqGkueDiFNZ4tHjRg8CR", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QPEbvVBWDG7qgy4smY8nWiie78Vec8qiT9", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QguNJ4evpN9i76dYHysTzLiKoWKFhE4B4U", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QbH8srDKeS5VcsQsgsaF3nqCzGT1NqfsTx", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QdtAQm1EGNgM7QDSaC2qvV9WdpRHwpApUT", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QbqRyFw7Xu6Nsb4FraaUSe7nUPukuUpekG", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QRFHr4jnVgvAsPTubeSrh8bPy1yzwzYaWD", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QWuW2YMygVtWieUo6a4yayD1xFDWdnmo5j", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QbqRyFw7Xu6Nsb4FraaUSe7nUPukuUpekG", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QSkiGy3v3MwvGy3aACuyLDv3Xy2AWAYfPS", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QVkQTr8PzDTaNxhvrC47UUxUi6V987a5qL", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QQPYyoE3Bm2vh8Wr5aaBNyirC8dd3BhBGH", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QSBJuNoCAFUTcevuCTcMi3i5nzNPhC5R4b", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QNJ8jDx6Mni2GKLHEY1BMh9xDumT3vnJQM", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QMLjFiPiPa46YBRoushe6a227kSrXnXyKb", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QgVZb632eqF1eLQm9gBGuBtyp9Dyz2FKUK", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QPREQjU2defiYdgA33HDiLNGBpxtuebeqE", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QNJ8jDx6Mni2GKLHEY1BMh9xDumT3vnJQM", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QMsKXQAYKmR7dBH4P3kMLiKzYatK3h1CeS", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QZvHW7amu5DNktsBgaMrR1brHZhhhVwKLW", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QgqM5bKs3tNqKNAnVeaQp4oaYMXCmX6YJr", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QhMEWhZCufhKLkfuNU2DAzj1mmWoAxX147", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "Qgwb5SLVGAperXAMoVNBDvGpVAxTVY7f6F", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QbvxC3ENqomXp11833APchdjeyCNd49nLj", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "Qj5ncwncQY4KPYgKHD1eYpXHbR717PeLcJ", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QcU4VhU9ohDXU4k4AUMapgJRYSzEpizjLN", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QZsygC1chppsJK1cnaHG4fEsNaDYfLF7ZJ", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QYGcPZcRhGaY1MsiDr3VtwTXmB9TAbLFSn", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QaFN7DWcGF7keNACJpwCVnegePbfsAoFCw", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QYGNMWBmqWgVtMWGHypAsKhDVQw5mrFZww", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QcNmqT8CZ6zSZwuRm5LahRZnuGBJRnPY8o", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QXv4WiqgryFPi8BgX7RU7gqtAgrVutmU4S", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QRKRk5HVADsN1LHygK7q2pA7dWnYKnPpCT", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QSGB4Rd2xhd6UmA9LALTQ4f89Tfsz5VajU", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QNVKrjEq5bZdiDtgo64m5kz87rTHqCwvCP", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QNMDKE7XTujNQkuQorcHXw6hL7qRvyaTjr", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "Qgwb5SLVGAperXAMoVNBDvGpVAxTVY7f6F", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QgCQq4cFaGrJhwvKs4XwvccKiLZ8GVMCXR", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QRtRELSSASzqiYy2FtNcrePH6TVnqJkv9B", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QMtm8wVPHGE3qHg2hMaj6SZ78D5eXw3VWZ", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "level": 4 },
{ "type": "ACCOUNT_LEVEL", "target": "QVrvy4ac2jBTfxyCKB7MLimqJooTDBApmS", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QNHdGeFJmPcDdN8prPzPL4bk2dpnJ2ZZFr", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "Qce2Djqrk2WzG1QhMZ3BqFok9HGsz4wtM3", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QeDhJRqTyeDmcgGJoms6FHK49ZGVpVahxg", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QUXga5K8nzd9EqYtvEesZWEYuA688h6D3d", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QfbX8JJupEw5ckNtU4upQgET35oLTr5e6v", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QRaDef6H2zYfefqLwYGmUg7T6DAqo6DDqc", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QibuD4c6gvXgS4iut7q3sXuVb23rgFJq2M", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QMozpRT9aUunfmPh7EtQ6LPoth2JFJWBXC", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QTpYQqRyMekaEuECziirzy3HvCVofZS1wJ", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QMuWNAJ2tbeViHtBUN3yD2KARrrzcanLAd", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QUGo9SErgc6ceB5aBzcSJDNqBkQ9eaCKZS", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QdFZk74skMUu4rKMPEmcSVwR87LNDe6o3Y", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QWLpsGYrkF2cy3tH6DCxso7kXZpZJvv13e", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "Qgp16aMcdiS2EUkxCm5NSZgB8DixGK51zT", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QScBgSw74MquesXmVJxerX3YgyhtShRr4q", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QjgGeEkyiXa43pyqkXxZbvAChQpVYfUyKz", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QYD3kXchZ86vUyJBXNCVQ4LUvTAd6PUZW3", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QWL7kZp6Pdd1bhxZ6SXPhVf5g7GParG9CC", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QbxJvwrEHZs7MDE8rbqBwZAZkcywue5F3W", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QUKKwug9PNai3DBggXUXP8Ag7WmR5SVUR4", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QWwrtjBL4ah965XPXHYJhymreC9jyryNLZ", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QjEaMxcBKMsj91ytKe6GdTBJP8Mu1Ru3r4", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QPusqAVBVFGAAeE7RdospttA18AuyLP7sB", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QSq8y4ZrSbF55ZddWNcw1ett2LDtjQEvNn", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QU8XJyQEZxSHgrS1XyooWUo5MmruXg36Pe", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QNw9xAm9TUerin9QsapCPL9mV6zmoXyJrh", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QYuMATkLjz7YB6s4EG1aWCmmmrAPj3W9Ce", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QNiTnonHpXTeUrgNdyYWVDPP4ZdjkLpW72", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QYuMATkLjz7YB6s4EG1aWCmmmrAPj3W9Ce", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QjEAs2or122weKppv5zALzoQzXxbsDjy3f", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QdXdUxnyKGGo7eEfTcx85oEikNe5nYnuwa", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QWBFK5h61ZxGfqQpEkwwKTcLAo8t9VWe4K", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QjEAs2or122weKppv5zALzoQzXxbsDjy3f", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QUZQPWhrxpze32vGiux6wa85kg9iwuhCDx", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QdwSxr3t4hdGHjQFy6EVGR9yGMipefsTuo", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZ2gi6BhUNpGmrErgJLFuY1WHy6xK1J7qX", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QQaKBSjAt9RK2bqJoSriR77X4ULstGzrFQ", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QU8XJyQEZxSHgrS1XyooWUo5MmruXg36Pe", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QXYk68x2tiUrDBv8eq6wd4KtBmLHYiC4zR", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QTyokTJrR4b2y76An3BFUEbqQy5vvg76iN", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QPyx2bNiAnJEjitfeAh8jZXzQVKio2B7Mi", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QMJwdufHY9dMoARHCUyGbMPAqUB4BcqGKm", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QNffmqjCUQsLaHLZXBaA47tQW7bERdpP4R", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QeUc5NfzubZD4eA4eJ7bfXAFraAWc7jVz5", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QeDhJRqTyeDmcgGJoms6FHK49ZGVpVahxg", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QMMh94Pfs5LVE4xJee1yggViqP1YDdQHT4", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QQHWFSJpzuDupPfcTvGMRNJp2UGz98Kb7j", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QdepeWWLMD3LiDRKkKBria29rDhZP9V5RJ", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QWez1VBfYVJ8KFoZ6MhJDzYVLbn5mr38VT", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QMMh94Pfs5LVE4xJee1yggViqP1YDdQHT4", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QU9dnN47Nc5KaH7JwNoCZ7TANwCW5VX9iG", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QXoW5M113C3SDUeFdjVVs2m1RT9XtzXS3z", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QjsD7pMh2LpmgYudaaRM8gmzhBC4c9uwyj", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZxcntxfJimHus9pgfvVVPbpHs5yU7ZMhB", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QRBVE5gHumH6RUgEUxJdQ5417NUvc14k3F", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QakZtV3nZeRp5UEoEUNX7p8Qz5VbVXTQid", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QXeiJa7ftN6dAYNZcdqKDBhmAFyGjX2zkm", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "Qc2eRuYXtpATrg87Pr1WDXkUBCsgNRYcQF", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QbEJ5tfeskASVnmLFeKzspuZJJb6cJVPZ4", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "Qc2eRuYXtpATrg87Pr1WDXkUBCsgNRYcQF", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QYpYshfVzMMVU71KqAV4erX3NJAUCnJXgD", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QViKVZa3M3ar7RBRSBMTx8FdzLh1zxUhN8", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QMb2cAKb2BUxknneuoXQynJ7uzosJ57Top", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QbGwYRV3UDek4VNpzoAVVQWfoKjZmm4qPb", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "level": 2 },
{ "type": "ACCOUNT_LEVEL", "target": "QYM76r3tuf6FJWz2USqVjwXJrV9tLMz1cj", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QfnbnWrRQ4HNDQvtg3wG2B1eC4ycUsFqZz", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QUdjqijDoyc83K4WcMW1sCn7zLd2t1WTqn", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QbGwYRV3UDek4VNpzoAVVQWfoKjZmm4qPb", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QRUbzEbLd7fRjAx2fBdXAH4QS1WQyetvDc", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QU9K6pWhkok9mokkPDoCwgSu3j62ECuKLD", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QaZs97g4Mbq9tXMoBWbhw3jFvBBVkWKS5F", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QcJwVCyzraPy51uB4xd4f94n2UFYAsznGC", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZbKMgYBwWGyTDJzoDir2aNqXsFdDp6fS9", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZ7wvWAUcHKRhvQ3ijdrqM4zucQKCgQ1hQ", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QgmEtScSZWJmTUAidCZKj6gDr3LznZ6rr4", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QQXgH4CnQCB76BbXhsApu6ShhohFfvoXv7", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZJc1V32oFm8tufB4bk7fa3aepu4EdkeDU", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZbKMgYBwWGyTDJzoDir2aNqXsFdDp6fS9", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QaZs97g4Mbq9tXMoBWbhw3jFvBBVkWKS5F", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QSkicapNH35a3UebSxxSMCfntBhwwi6veW", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QLqZD8eiNk8nrzyWcURfQDS9T5NWx98vrz", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QQzMut6erjgSKCpZ1dHDcjKcj9KAce7cug", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QUdjqijDoyc83K4WcMW1sCn7zLd2t1WTqn", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QfnbnWrRQ4HNDQvtg3wG2B1eC4ycUsFqZz", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QU9K6pWhkok9mokkPDoCwgSu3j62ECuKLD", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QcJwVCyzraPy51uB4xd4f94n2UFYAsznGC", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QVuksgNt3QAr7KCrkxtE5FWrczfgLKxs4H", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZJc1V32oFm8tufB4bk7fa3aepu4EdkeDU", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZ7wvWAUcHKRhvQ3ijdrqM4zucQKCgQ1hQ", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QQXgH4CnQCB76BbXhsApu6ShhohFfvoXv7", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QXpCzKnANyh8jrRRxemvsXHAeFRHrDvdXF", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QVEfMEeEHP768c9rEyi3WcH2JQwqheVDe3", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QehM6hQZVkrYAst8WrBVdQiMFfTtDtbKQu", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QY7gQwZKiPYMHYjYASSDBhhAoyYaxmex17", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QXqAtZJsLU6HFGTzuegjeUbmEW8cGEnYt6", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QLqZD8eiNk8nrzyWcURfQDS9T5NWx98vrz", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QPE3EGR8LTYSLtWBq52SFte66oud6cxchF", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "Qa9NXeLAvmaTsKackfDRmm1A9zy7JeEjgV", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QXqAtZJsLU6HFGTzuegjeUbmEW8cGEnYt6", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QXpCzKnANyh8jrRRxemvsXHAeFRHrDvdXF", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QVb7P596imucSwDqb5HHEjqpnAre615PbS", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZgDpQ1Vyi5pNNe2ZzBRhcUgSU13yeWvmH", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QTgfJFaEj5wuFvufwjUmYPhQ8eVfHR4u22", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QNMtHRjEWPgZUVCpiC2qE2LyJsxq1BC1Yj", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QUNTxxwr9f47RcwY48ZUP6FmmKnWCsxes5", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QiQernJdiP4zCECB93v247FUtovkfJyTYm", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QSTDdguUfKe6TaQrik2zq4Xrbu6unxNa9o", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QVb7P596imucSwDqb5HHEjqpnAre615PbS", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QQfFiVWkyAmfs1hDUcWU6XBfonxLdz6RSN", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QgsxpySDm1qja6D2EyKuHPiUhyTM1RMk6c", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QWZwi5NUzHuJfg5fh9HzjWRpQc1fmMknAh", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QMb2cAKb2BUxknneuoXQynJ7uzosJ57Top", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QVuksgNt3QAr7KCrkxtE5FWrczfgLKxs4H", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QP9vU5yTsBjuTSFxH5Cb9VXYNRHKhMNAJ4", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QXm5e16Lq6dnYwpZJ8Rn2cME3ziHZfRRnp", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QWkLaA4fYK51m4CszSgBd7efoDsSKamdf1", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QQzMut6erjgSKCpZ1dHDcjKcj9KAce7cug", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QZgDpQ1Vyi5pNNe2ZzBRhcUgSU13yeWvmH", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QiQernJdiP4zCECB93v247FUtovkfJyTYm", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QUNTxxwr9f47RcwY48ZUP6FmmKnWCsxes5", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "Qa4KygekpW1Brr3D66K6rGtS5AY8qkhvD3", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QQfFiVWkyAmfs1hDUcWU6XBfonxLdz6RSN", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QViKVZa3M3ar7RBRSBMTx8FdzLh1zxUhN8", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QgJDnJF1zfoFBxB1LRvi5Rxb8p345UmVos", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QWZwi5NUzHuJfg5fh9HzjWRpQc1fmMknAh", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QNMtHRjEWPgZUVCpiC2qE2LyJsxq1BC1Yj", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QP9vU5yTsBjuTSFxH5Cb9VXYNRHKhMNAJ4", "level": 1 },
{ "type": "GENESIS", "recipient": "QS2k8PMpmvUHzFDp2JfbVxRo8SiGiV72xx", "amount": "637557960.49687541", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUM95jekFMye3Lmkg2gj7CdhFreYye4DaA", "amount": "400671036.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaYAUoT2SzYA6kqBEW6W11brkYpSf7g4Wy", "amount": "352652351.30705076", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdoLZqB3pY4NgbSpvbsFzBDq2LcShjXsoq", "amount": "263574650.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTnPQVphizFhBFtSbTQzAC4Kubj4uqpv96", "amount": "232356835.62157121", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMtx2UmUuRZckCmRJRyxdzSAazHP8hU5rA", "amount": "160672815.43629771", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVz1rKM9QQrGX7gkxR9EV8a2T4AuD1QT87", "amount": "128506517.58266672", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUUcETWLi7EcjQ9M83Aur3SwmESFBFuQye", "amount": "126998534.24906898", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qa15fMdEiY7WqK2xwvdHrdhU2TE6yk3V3L", "amount": "122690068.33700012", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QM4LF9EzQnXJ9VyFwnJbJzskJLrybuSzsw", "amount": "114361793.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQKeokRiFCgAhBSdu1DUf5e1LCkgApvrxZ", "amount": "100000078.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QiaaoNZ54wKoaUMXxW72UsPt1MiPpeUTWm", "amount": "100000031.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYoRWAxw6CVMeYeWHKJh3csmTVkVzjpdBo", "amount": "100000026.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPwLkxemtGsrdcPJSA4iMgKbBPdecL7dwZ", "amount": "99999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMkgf9Y6Ac2TUrynDvyhX69ekpC3P3GQmN", "amount": "99008835.47860426", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUnfm6t9SrhgFB8YqCV1vQhmqFzhncxioK", "amount": "80000207.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWZm17rRXeUehcM4TprVNNRSTHWQmG2bME", "amount": "62663714.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXdcbgN6vdQRBKJzHSB2JaSDDzPEsTUi14", "amount": "49450000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QczMrVLEnEAXwvucwrfasH9H7QHNMPxLKu", "amount": "46488501.53473695", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qaa5hs9dj1gPxhqigH7tVyZ3oRuVcMULXB", "amount": "44766868.70000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQMswzmv65MZSdafqzCACm3tbLj23ixkVR", "amount": "42447489.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qa95hURaNK4kPhDhbdmDFm2wMkkoWFZ4Zz", "amount": "40976709.97984710", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVEde9kJt2ejqXavBzK35zY3eztnkSXTwN", "amount": "38172492.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZjDFrMrgeR9SGL8bprqyiXVtAZMVCXESZ", "amount": "37291276.42323117", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPmmPjdDXW35ykF557KbaVy5jYBbaM3pRp", "amount": "33333332.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaWReiLe5tAsTNoq7hMiSGNasJEXTX5bmn", "amount": "31465829.00562504", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLrErVJvpRdYAt4dGwk8i8rG3PMUSgpp3i", "amount": "26593505.67231102", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYsLsfwMRBPnunmuWmFkM4hvGsfooY8ssU", "amount": "25303386.66951814", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgULJXk3qyLDr4EKkz48rxwwvWK7BDL8Ux", "amount": "22476856.86000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNwedkYs6hToBRPZvBb6naKvDEgB7BeaKi", "amount": "21967101.30582773", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgP7vQABSDJwuE8VTdVkiNnbkorqWWctQm", "amount": "20442665.63310002", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QM5Fg3kBzj9BqsGAJKHhsEtgRgf4x2Yn5G", "amount": "20000006.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLdC7wm9PYC1KjgZaeVdGzH4M88BYhEeEw", "amount": "19927901.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLgBTDkt7t7WwbTd1RM8iGxu9FexPZcn7C", "amount": "19000025.40000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQShonrAdJXg4CzhegtynzaBP7S631vKGq", "amount": "16577946.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMFJGxGLCbegHL3NZ7HRo3UYaCXhweJVKS", "amount": "14977498.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QcAVj3i3drVkiC9P8FtDgVdYz8Zeu61uNg", "amount": "13142764.62938834", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTDB2mmxEU7L4oUnmBh9F3qhCxfVpJUzaA", "amount": "12960806.75574690", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLgewDr48AZxWbDbRZ12xEvCePwPj9bMBE", "amount": "12684967.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPEoMF2dA7NHrHhsSG9zczCFwx9wFdWvzT", "amount": "10033147.61257500", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQSiZAgdMjadXZRVTHa252AAY2yjZfQKR1", "amount": "10000000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWLhUT55hZHgdD7ayEqwHnF9h62dHV45G1", "amount": "9999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QjnHejJPyuyTRs7huzcqXtbM6sufWyLf9o", "amount": "9999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QjJjjBUJSZAMuYiwTyfJTFthH6SrofjG6d", "amount": "8871800.22712502", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRh9zNPmz5McxeWx3iCy2xnBRrBoYDZfMJ", "amount": "8739995.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWGwGBzZG8UeNXJYZq9tjzN6ceriqTgMRd", "amount": "7825760.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRj4VNEthakckhYpCJMEBhEFk12pa7GPJT", "amount": "7810001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbdHzyw6q2hMAhg12JFbXSEcbrj43xNnYG", "amount": "7593814.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNFWWhmsun3HVUChjck8XqhsQW2tckNxxM", "amount": "7239123.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QeosmemBtE7aosv2QZhq1PTjqpKurKXKzy", "amount": "6185646.80750010", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QU8SzA12SVKPgqpTzRna5itKktGhvEdmgP", "amount": "5931486.64530000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQM9B3ipQcSvATsxb4vgVuYs9kuxgnuBwG", "amount": "5032347.69657146", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLu6X6CVhr6tovNR6k9pQijxN4DHiuLeJS", "amount": "4918400.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQ44LmLWzkWWe2UJfdvsySkd4ZRs1is3sX", "amount": "4384750.69674618", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNK7x1kgiA45Zvc5jZFaoDeDzrBrxivwYd", "amount": "4135907.02294526", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPdkHcN8DBLuxaFf4Q4zXNn3rq2G78VgNt", "amount": "4128493.31116798", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMNdPz11XubtvxXLGeiG3PHKaQW67LkZMp", "amount": "4056950.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhUWSWWFt6vDy2qNFn68JPTPLjyDrzrh4D", "amount": "3220564.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQ7wKkXyQtkwrgG6zeSHNrnDhXQ6fGUsYS", "amount": "3088411.29750001", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QcxXT317FQi3mq9hrQggw7xZujPX63XCq7", "amount": "2779141.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMTcDA7v1dbRXHkryn4ppiM9Q7LGZFNkVT", "amount": "2752011.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTekAqCoXPy5R7NE78GXxjSaXBuekZge88", "amount": "2740816.93000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVQErjTVBkPTphbyDhPvhHKGX2etNXZsgy", "amount": "1999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNPfWoyuvYaScXuCdiCM2PEzvD7PWybBvh", "amount": "1749402.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaGhALQm9h3G6m728ZM9L2rrE7Y63nk9e8", "amount": "1637480.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QV96D15Y3qM1r55odiDf7j2TTYY8jCMJPs", "amount": "1572208.70562500", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLsEy8syg8EXttzajE8AWGg3CxccNR6YML", "amount": "1300001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLeNqYjzid2oJFL2iVtcHeAkyunuTsHae4", "amount": "1185185.44326193", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaUhQiyBZpZWSmFo56zRu2XXfZVL4D2xii", "amount": "1033451.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMrECKgkohx6ZXEMdLzikqBmAkdyHeQDqL", "amount": "997498.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QR1GoYcR3iasGNWtJjeNcZFPf9BH4TjCeC", "amount": "858153.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNsdyTKN7MVsTYqSyZNPatzRTgNyWDqPZW", "amount": "832163.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMK8v74dCqJ3d3p4JMZZknvQ1wEJovHEjd", "amount": "805444.62885757", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMFysMKXLubP21BCYeYiSNP3v5mLhp81Cd", "amount": "676881.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMdfkckSTcCMKnRRve51SEfZQPVLZg9S7b", "amount": "333086.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QN2UFyKfUsJBHmM5hZrrD2wrwZ1SctSfwQ", "amount": "296438.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMrSxECG64fp2JaoXkw9DYEp6jFPYmh5Jr", "amount": "124684.50000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQRUVwMGifreiLv3FuPTTcA1WbsvhH7Mux", "amount": "102825.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMQAG28pyv2aZVjWbKoRn39Ytir6rLPnTK", "amount": "100745.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWY9HkkCvbKJaqWMEkLbbAJcncywJgy31Q", "amount": "100004.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QV6rVM4Ywh7TzdN1hgSWVrArKxyEUMuMfs", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTBjfXU6ctuRFBTGkcJySZLqB3L9BdxNbb", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTmKWUXtuQ3ihfigtuLSeFddskmSy3fGN5", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQDpKmaKa4uz61CSGVD2zSFaQzeRjaQ8FT", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRQvk1PCt7pNm6JSohtEQQdMEu7BrWjmtr", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVdEoLEamBDg8MC8uoMe6fsWrpbd9J3mTL", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QW7o4Cd6tvFLe2WryK4eNmFCxz7a9Bs5pJ", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWCTesfyodV4CnKF5cP1RCnksEonZhyJEj", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWCEJXDjfZLUfgCJ5V6Uy16KkdigLqEvvo", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QV1FfiEGhYRfNSN5HeXL6ZB1t2NeQJ761N", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVjAVUkKjFHX9N2UQntuPWqHe3PGdBgKgx", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVEuVYqdLfkAyTU5paKkSj2kGid5X7upPi", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQEBW8LU48cuV4uiwQUJ94k3UkJo5eszeg", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUqDXJzwKrte2YNpR4b44ttKWwnSk6pRTZ", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgkNZyrj4zAEAwCDZHTLQ7RcbcCTwcNzPL", "amount": "99999.09400001", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QW4jfDfsVWHLeLLvdapCG23q5MGajh8mKr", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVvq5dE352hhujmDcoj6CaEajp7mBuAuKK", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QU6Ac4sVimeehkuK8XcbpFUBVatZ2qgd47", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQodoJ4PdvwWbPwLErJsXjxtrW2BDZ52Gd", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLm1JSR6mph8vXrJ4ZHj5PjchAP7UoN9zm", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QU54p4EUyEr6EDEK7tmWEgwMBkr5MHNJr9", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMAsEHJcNAk6Vkzik2bEV6nJ7JZjWAksvi", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTAth8ahCmt7ZchLA2N6S5jbF5wCMzyC8H", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQ5jfYBm2tPH4xCdfWDccnzUxhNPHL3VS3", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVfFNmHCgWFHhUwLBNBYp32JjL2uusyfsW", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVfru93ZBLd5XR9YzRBbFHCuYiNV1hJhEw", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWA1V7vrRLuhibXjRCSG54y4mkTLf3a4T7", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVxFXg9QMJp7FFD4XPU2BXR4hySCB9u9Zm", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRMpfSZ3gcX4gZKCXHam36qpaCC49F2cEt", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLecTtNrkgGFAwsSPVpJLgkqdjmjLmsT3F", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUVkDZ2ercosZrRBnC8YZt7et9sXfwfKP4", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWR8npVGu3w75p5WtgxMikcnujVHEuPBxL", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMHjTaHRi1zTRgXq1vddRiuMBxZpHEJFeb", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLpBVjVAxhygwbvHyGLLnKbyvsZ1jUqaWC", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVXchvzB4PaaXSwhyZcayiTTSe5cje3DE8", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRWVqUTDFQRRAKiWjkFCk5pdE1iDw2Cz8s", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVKtHgZutURJdqnMCRRewsi2WsFNmYGoEE", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQAiKxdyRuKT7vPTQGbTitiN4E1WVnWZFa", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSu3hoF3n9qjMSBfmEn6bGVKNXpn4SrtiX", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQsuUfQg57meYToLCx7GTH2DYfAfe4KgQx", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRPAESx3b4pWw9C4Q6YLdNYUcCQsWbzEfE", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRPnVABwiqT3qBqC6bscfWU6R79RjhUfp9", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QR3mxAS6xdwsRGQXiMVngYmL953gdKcaoa", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRSeGbHVKqhbUGNzF33DhoJQiPpf9WxZWR", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRdmMSmXk65qRYD8r1yzMBSKFdZf89nmZ8", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSYyv5yJBhkrGeB33NV3NbRYTys9mfkzze", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQMFd2LfNJCyH6jkQkrK7mdKpjWiJhhFty", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QP9tgb3pN6FzU3UCmHGW95s9Fmgubzm8md", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQszEg4sqt2o5oN4pdyjxrAwGD3TY5QNfZ", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNLsfHsFcS5xc7BR1fD6aNSZLPt8CEKLuG", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVuKDbWVeM92yEta1fagk79NGQsLkvKdCf", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSCdg6rRRm4jb3r4XNKVytyQAgiTxkAjTw", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMdEATyg41BFbWuMZx829uzBZ8fqYDDERw", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QM66cjqmF3WCbbiac6tVU1pLupv2AvU4ev", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVeQRaWcPUNfT3yPWtm6xmuy1hM4N6b1yg", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPbWwDxBK3m1LU7uPv55Rq8bwvqLsSjXiE", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QR2cz8ErrLtcos58iKrQA9aPa8T8kaGc5w", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTrMWWXU6WyuBRW3WU6duNXpe7AbNog4kP", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWHinsRV49Nf2ue2ZBumK8jWHWrSFhHr91", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQa6YP9ocmaMBZPEAPnY7dDekdqo9os4gr", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSqcH6aiqnGnP8iGG1cRxMSqtypsqgWprm", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMrodSbGSU3zsJVi8BN5X5gPE2xvXGt1ja", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPJbzQTJ7T8fRFu5NbGu9goLZyeLq5Kg3i", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTHLtnDuCz85Xa2jkPuZKhdvyXh8yZH6NJ", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMMsPuLhww36VQAphKyo29BfhHLeekf8ST", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPNuGywKCewuy66DXj7GAJrev5HkcVWjuu", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMTN4ALHmKeJkbVFQQyceEAideDTb1eXdq", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRMbQS8Tz9tEH5Wxg6GcoqJ22NqVEcVLFY", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QW1NHhKUUPGg2WwMxNyV8VbzmEJJfBTXvy", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQginD86frBSnoNjHEk7JE4zaBWbTqnHk6", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPSEbEbncD9aUrSzNie658DuoFgex641Sd", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLzhvyT3LHzE9DqFrSwNivKMzkB6T1g2DD", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNG1SXqh2uJ4FAy2yxEzUXbRh9Z5KsTvq5", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QM1KcVDtpkfsrSyDx6v9okdigbXrNTDKmZ", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLnyXdhGDVEBmByTtjTZihVE3UnReghNyd", "amount": "78296.75000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTHQ9pFMoraWTCS1tuFzuwWSXu7uVJ18VF", "amount": "40395.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfNkfy7kZ2gwue4Vsoz3YfARwSCS13cwXN", "amount": "14997.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QN9QJxXAT2ceDwgXvSvjkndeUGXW84xfhQ", "amount": "8505.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVZAQrQHPD8rePZ5hWKx4uj7Ty4GEBe15D", "amount": "2989.50000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPZTxWtCmH6Y6zwwntjnPDfKG6zNKRivqJ", "amount": "1282.61375000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qgwuvmkqf1wTAxqyFpvUswDqgzWrC6towa", "amount": "0.66600000", "assetId": 1 }
{ "type": "GENESIS", "recipient": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "amount": "637557960.49687541", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "amount": "400671036.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "amount": "352652351.30705076", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "amount": "263574650.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "amount": "232356835.62157121", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "amount": "160672815.43629771", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "amount": "128506517.58266672", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "amount": "126998534.24906898", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "amount": "122690068.33700012", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "amount": "114361793.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "amount": "100000078.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "amount": "100000031.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "amount": "100000026.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "amount": "99999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "amount": "99008835.47860426", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "amount": "80000207.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "amount": "62663714.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "amount": "60036435.28601510", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaPKuyyQtXJcsVhKLKgxCcYewxwaawxLrB", "amount": "49450000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbVGj7U4C2tQfipTss4tSzC22bbrF6fNnL", "amount": "46488501.53473695", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "amount": "44766868.70000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "amount": "42447489.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "amount": "40976709.97984710", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "amount": "38172492.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWomBbcXNTdkyuPFUafwtBfpbxHzmUZzqi", "amount": "37291276.42323117", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "amount": "33333332.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QP8xG56L8b28h1mguSk9LuzNhxbHgAoL9b", "amount": "31465829.00562504", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfmM8dgfikTB2FYVuJ9owzQXVm8wP7T4QT", "amount": "26593505.67231102", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "amount": "25303386.66951814", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYhvqnVFapMA3s74Mu3D1DwPkEkcy1oKPn", "amount": "22476856.86000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "amount": "21967101.30582773", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgJDnJF1zfoFBxB1LRvi5Rxb8p345UmVos", "amount": "20555555.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRBVE5gHumH6RUgEUxJdQ5417NUvc14k3F", "amount": "20442665.63310002", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbH8srDKeS5VcsQsgsaF3nqCzGT1NqfsTx", "amount": "20000006.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMuWNAJ2tbeViHtBUN3yD2KARrrzcanLAd", "amount": "19927901.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSBJuNoCAFUTcevuCTcMi3i5nzNPhC5R4b", "amount": "19000025.40000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "amount": "16577946.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QakZtV3nZeRp5UEoEUNX7p8Qz5VbVXTQid", "amount": "16000000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZvHW7amu5DNktsBgaMrR1brHZhhhVwKLW", "amount": "14977498.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUNYcKorTAjcFEFH2kLuGzTHDSXHbTm9n4", "amount": "13142764.62938834", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXUWuZ2oAUodMU8EAQkAkDwkQHS1SFxpts", "amount": "12960806.75574690", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "amount": "12684967.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXoW5M113C3SDUeFdjVVs2m1RT9XtzXS3z", "amount": "11583150.66666671", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdepeWWLMD3LiDRKkKBria29rDhZP9V5RJ", "amount": "10051008.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qi74UrbLEsCD9D98rRQrsszNtRA4pWbXaN", "amount": "10033147.61257500", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "amount": "10000000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYpYshfVzMMVU71KqAV4erX3NJAUCnJXgD", "amount": "10000000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QeaDGU85fpffwsw9ngmd98QsT6NaFyFFed", "amount": "9999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qeq85FoJpxtzoDM93WiNQQCXiuiFynRQzm", "amount": "9999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbwgBD6LWdk1hZsb8EwdVVmDZdvpxMzyGT", "amount": "8871800.22712502", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaFN7DWcGF7keNACJpwCVnegePbfsAoFCw", "amount": "8739995.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfjL32jLsxtumbfx6ufmfCFCBccVCQFkrh", "amount": "7825760.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNMtHRjEWPgZUVCpiC2qE2LyJsxq1BC1Yj", "amount": "7810001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdtAQm1EGNgM7QDSaC2qvV9WdpRHwpApUT", "amount": "7593814.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbEJ5tfeskASVnmLFeKzspuZJJb6cJVPZ4", "amount": "7239123.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZfctgpdTkoCcLzpeWSSq7nYQwCLPsoFsL", "amount": "6185646.80750010", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZsygC1chppsJK1cnaHG4fEsNaDYfLF7ZJ", "amount": "5931486.64530000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXmYaDzKQdGiAMncJCr1FqXy6tX3avMRm9", "amount": "5580161.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgpAW6uqwNR58gNYWRCVXNLm4F5TuckAw4", "amount": "5389659.68181819", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLqeoXwo7RMJUsTv2sKbBffjDvbSSQN7vH", "amount": "5032347.69657146", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhjovpBRoW2BJ1vRf1xmBcEqENYzexryM6", "amount": "4996995.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQNigUeJxK9WMJoGhydKpfa5y2mQp6bX7v", "amount": "4918400.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMfWg9oJg49izXMeRWrsErgNnBD6mJcKiX", "amount": "4418544.42625000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "amount": "4384750.69674618", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXeiJa7ftN6dAYNZcdqKDBhmAFyGjX2zkm", "amount": "4135907.02294526", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QLsQg11ipfS4ZaD3QTFwncEgCBPsr1r2hj", "amount": "4128493.31116798", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QY7gQwZKiPYMHYjYASSDBhhAoyYaxmex17", "amount": "4056950.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "amount": "3220564.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUJhiF8h6ubsXhyATKHccLwD9cz1ECDpaD", "amount": "3088411.29750001", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPtJ6prBJ1NR5NbtuEj7kMaM1a9FALueHA", "amount": "2779141.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhAkKZZF1ZuC9ADgpfZ8BMZRVV43BiQhuZ", "amount": "2752011.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZxcntxfJimHus9pgfvVVPbpHs5yU7ZMhB", "amount": "2740816.93000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXHEZ4axuNq91K5wW9zaNSvtLzsdsQ1yVz", "amount": "1999999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPhNJXXc97CRBP4JMu2cgiumyfKciBHHyu", "amount": "1749402.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZu4Ka4XHKgTJMc6Y8Kr9cjF12sMKQaPQP", "amount": "1637480.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVEfMEeEHP768c9rEyi3WcH2JQwqheVDe3", "amount": "1572208.70562500", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhdoF3Kt3dV5DkuPgTmvH3RNzgZrSK9o6W", "amount": "1300001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQ5qnof5pUgJem8NPsAPgYdENL88cNqSj9", "amount": "1185185.44326193", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdceL6F94xZCUmXgrGPKCrsvtPtKmFprDp", "amount": "1033451.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qf2KXaB68Ca6E3uYty14DrEVXGJ6RMg8Tw", "amount": "997720.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QiFUUj4GvfHTTuAhFseuoWZm3wYemqxSDn", "amount": "997498.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXjRdpB4gMjixwP7cv5yeyn8RQR9BuDhet", "amount": "858153.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWez1VBfYVJ8KFoZ6MhJDzYVLbn5mr38VT", "amount": "832163.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPE3EGR8LTYSLtWBq52SFte66oud6cxchF", "amount": "805444.62885757", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZh58Nw9KfPHyhTbVa37iuqAsPGDSzJtka", "amount": "676881.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdUkv47jHcpCB5mZvqEn47XZPzBnxfr8V5", "amount": "333086.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVCzURyACQvd5sYkmMbg35YJvtZjsr2uGC", "amount": "296438.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfVQpcqzK6RVRWESrrdgyVNmM9sPuxjBC5", "amount": "124684.50000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRMSN3DDrfGcBpqAUfYJb5xz5DovCL2Qy1", "amount": "102825.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWKYjxBUt2c6BHm26c4k7U8iF9eUEEAeQy", "amount": "100745.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qbcy4uyMkQF2JXYqGkueDiFNZ4tHjRg8CR", "amount": "100004.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMtm8wVPHGE3qHg2hMaj6SZ78D5eXw3VWZ", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNMDKE7XTujNQkuQorcHXw6hL7qRvyaTjr", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTpYQqRyMekaEuECziirzy3HvCVofZS1wJ", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWL7kZp6Pdd1bhxZ6SXPhVf5g7GParG9CC", "amount": "100002.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMLjFiPiPa46YBRoushe6a227kSrXnXyKb", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qgp16aMcdiS2EUkxCm5NSZgB8DixGK51zT", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQPYyoE3Bm2vh8Wr5aaBNyirC8dd3BhBGH", "amount": "100001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRtRELSSASzqiYy2FtNcrePH6TVnqJkv9B", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWwrtjBL4ah965XPXHYJhymreC9jyryNLZ", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMJwdufHY9dMoARHCUyGbMPAqUB4BcqGKm", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qce2Djqrk2WzG1QhMZ3BqFok9HGsz4wtM3", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "amount": "100000.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QaSL21TX9QH3NTBTXbNCysB2zWRk3vkm6z", "amount": "99999.09400001", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUXga5K8nzd9EqYtvEesZWEYuA688h6D3d", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYD3kXchZ86vUyJBXNCVQ4LUvTAd6PUZW3", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSGB4Rd2xhd6UmA9LALTQ4f89Tfsz5VajU", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRKRk5HVADsN1LHygK7q2pA7dWnYKnPpCT", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUZQPWhrxpze32vGiux6wa85kg9iwuhCDx", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUKKwug9PNai3DBggXUXP8Ag7WmR5SVUR4", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgCQq4cFaGrJhwvKs4XwvccKiLZ8GVMCXR", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QjEaMxcBKMsj91ytKe6GdTBJP8Mu1Ru3r4", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgqM5bKs3tNqKNAnVeaQp4oaYMXCmX6YJr", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYGNMWBmqWgVtMWGHypAsKhDVQw5mrFZww", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QScBgSw74MquesXmVJxerX3YgyhtShRr4q", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNHdGeFJmPcDdN8prPzPL4bk2dpnJ2ZZFr", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdXdUxnyKGGo7eEfTcx85oEikNe5nYnuwa", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgVZb632eqF1eLQm9gBGuBtyp9Dyz2FKUK", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXYk68x2tiUrDBv8eq6wd4KtBmLHYiC4zR", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPEbvVBWDG7qgy4smY8nWiie78Vec8qiT9", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdFZk74skMUu4rKMPEmcSVwR87LNDe6o3Y", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRaDef6H2zYfefqLwYGmUg7T6DAqo6DDqc", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPREQjU2defiYdgA33HDiLNGBpxtuebeqE", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWBFK5h61ZxGfqQpEkwwKTcLAo8t9VWe4K", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QjgGeEkyiXa43pyqkXxZbvAChQpVYfUyKz", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QcNmqT8CZ6zSZwuRm5LahRZnuGBJRnPY8o", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSq8y4ZrSbF55ZddWNcw1ett2LDtjQEvNn", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPusqAVBVFGAAeE7RdospttA18AuyLP7sB", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNiTnonHpXTeUrgNdyYWVDPP4ZdjkLpW72", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNw9xAm9TUerin9QsapCPL9mV6zmoXyJrh", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbvxC3ENqomXp11833APchdjeyCNd49nLj", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qj5ncwncQY4KPYgKHD1eYpXHbR717PeLcJ", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNVKrjEq5bZdiDtgo64m5kz87rTHqCwvCP", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMozpRT9aUunfmPh7EtQ6LPoth2JFJWBXC", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUGo9SErgc6ceB5aBzcSJDNqBkQ9eaCKZS", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QQaKBSjAt9RK2bqJoSriR77X4ULstGzrFQ", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QZ2gi6BhUNpGmrErgJLFuY1WHy6xK1J7qX", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTyokTJrR4b2y76An3BFUEbqQy5vvg76iN", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMsKXQAYKmR7dBH4P3kMLiKzYatK3h1CeS", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QibuD4c6gvXgS4iut7q3sXuVb23rgFJq2M", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfbX8JJupEw5ckNtU4upQgET35oLTr5e6v", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbxJvwrEHZs7MDE8rbqBwZAZkcywue5F3W", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYGcPZcRhGaY1MsiDr3VtwTXmB9TAbLFSn", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPyx2bNiAnJEjitfeAh8jZXzQVKio2B7Mi", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYuMATkLjz7YB6s4EG1aWCmmmrAPj3W9Ce", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QcU4VhU9ohDXU4k4AUMapgJRYSzEpizjLN", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QVrvy4ac2jBTfxyCKB7MLimqJooTDBApmS", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdwSxr3t4hdGHjQFy6EVGR9yGMipefsTuo", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "amount": "99999.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXm5e16Lq6dnYwpZJ8Rn2cME3ziHZfRRnp", "amount": "78296.75000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QeAa7yawpJqQYk7PNisVD89HezskBRecH6", "amount": "49996.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfjNGq3SQt17Wi3e1Qp3cHJVeHMUHTe3Ar", "amount": "40395.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYogZKQbwwQV2ym46L53RYNGp3WfACCufj", "amount": "37998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdyyAwnpPEXtHmcz6GMpDoJZmP7rR2EwF1", "amount": "35998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QdceyQF75uirtzG9s7ZD2EeTMUAhWsoyFz", "amount": "35998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSsCvzpJhynqbNceqh2AYiLRHRZnCWcR9b", "amount": "35998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXRzrQyhShKoVUUwzGEMJK8p5RZqps8wa4", "amount": "34998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QSzaKcGZscNAGR476Bi5VBKqv5meNPAZQK", "amount": "23998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPLPWkys8cyZicdacFreT7TT3a2Jg6dzL6", "amount": "22998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QbBfYeGxPXpNkRMF5TrHA7tccjxCBeX5Vg", "amount": "22998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QXKaS5ydZ4VRGrEvMr94FvNzf7onEbxetE", "amount": "22998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPfJ3FU6jnamrxZJDH3ijH5Tze8UoEmtGu", "amount": "21998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPZdi91axpQ9mkPbJdhGGjssmT5jaaZNX2", "amount": "20998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhUWEeKX8EAxX2KomuaVmV3NvZrAV4Eznk", "amount": "14997.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QUg3gHTxunZzZMv5j9BmNoGrDzQbC3tNtN", "amount": "13997.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QNryEH4ryCCMNqdpnSG7qdj1Yr1NN4dRgU", "amount": "9998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhyZ1DUi2vg4JmG6aLHxqsx2TfzhqCs7L6", "amount": "8998.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qa9SH9rEKGDvJakQJiiNp4J1P8XnzYRCAn", "amount": "8505.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QYFxjNYARmD7d3VGwXmfcdejyZ4pvUUs13", "amount": "2989.50000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWZwi5NUzHuJfg5fh9HzjWRpQc1fmMknAh", "amount": "1282.61375000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QTKFuqgfLCn2JxQq7A2WvLjoYGGMWmr9dj", "amount": "997.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QfwU5G7SHXNaGNmK2YvwieGpG2UsoPibB8", "amount": "0.66600000", "assetId": 1 }
]
}
}
}

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<localization>
<context locale="de">
<context path="Api">
<context path="BlocksResource">
<!--<context path="GET signature">
<translation key="operation:description" template="returns the block that matches the given signature" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET first">
<translation key="operation:description" template="returns the genesis block" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET last">
<translation key="operation:description" template="returns the last valid block" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET child:signature">
<translation key="operation:description" template="returns the child block of the block that matches the given signature" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET generatingbalance">
<translation key="operation:description" template="calculates the generating balance of the block that will follow the last block" />
<translation key="success_response:description" template="the generating balance" />
</context>
<context path="GET generatingbalance:signature">
<translation key="operation:description" template="calculates the generating balance of the block that will follow the block that matches the signature" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET time">
<translation key="operation:description" template="calculates the time it should take for the network to generate the next block" />
<translation key="success_response:description" template="the time" />
</context>
<context path="GET time:generatingbalance">
<translation key="operation:description" template="calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance" />
<translation key="success_response:description" template="the time" />
</context>
<context path="GET height">
<translation key="operation:description" template="returns the block height of the last block." />
<translation key="success_response:description" template="the height" />
</context>
<context path="GET height:signature">
<translation key="operation:description" template="returns the block height of the block that matches the given signature" />
<translation key="success_response:description" template="the height" />
</context>
<context path="GET byheight:height">
<translation key="operation:description" template="returns the block whith given height" />
<translation key="success_response:description" template="the block" />
</context>-->
</context>
</context>
</context>
</localization>

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<localization>
<context locale="en">
<context path="Api">
<context path="BlocksResource">
<context path="GET signature">
<translation key="operation:description" template="returns the block that matches the given signature" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET first">
<translation key="operation:description" template="returns the genesis block" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET last">
<translation key="operation:description" template="returns the last valid block" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET child:signature">
<translation key="operation:description" template="returns the child block of the block that matches the given signature" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET generatingbalance">
<translation key="operation:description" template="calculates the generating balance of the block that will follow the last block" />
<translation key="success_response:description" template="the generating balance" />
</context>
<context path="GET generatingbalance:signature">
<translation key="operation:description" template="calculates the generating balance of the block that will follow the block that matches the signature" />
<translation key="success_response:description" template="the block" />
</context>
<context path="GET time">
<translation key="operation:description" template="calculates the time it should take for the network to generate the next block" />
<translation key="success_response:description" template="the time" />
</context>
<context path="GET time:generatingbalance">
<translation key="operation:description" template="calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance" />
<translation key="success_response:description" template="the time" />
</context>
<context path="GET height">
<translation key="operation:description" template="returns the block height of the last block." />
<translation key="success_response:description" template="the height" />
</context>
<context path="GET height:signature">
<translation key="operation:description" template="returns the block height of the block that matches the given signature" />
<translation key="success_response:description" template="the height" />
</context>
<context path="GET byheight:height">
<translation key="operation:description" template="returns the block whith given height" />
<translation key="success_response:description" template="the block" />
</context>
</context>
</context>
</context>
</localization>

View File

@@ -1,72 +1,14 @@
UNKNOWN=Unbekannter Fehler
JSON=JSON Nachricht konnte nicht geparsed werden
NO_BALANCE=Guthaben ungenügend
NOT_YET_RELEASED=Feature wurde noch nicht veröffentlicht
INVALID_SIGNATURE=Ungültige Signatur
INVALID_ADDRESS=Ungültige Adresse
INVALID_SEED=Ungültiger Seed
INVALID_AMOUNT=Ungültiger Betrag
INVALID_FEE=Ungültige Gebühr
INVALID_SENDER=Ungültiger Sender
INVALID_RECIPIENT=Ungültiger Empfänger
INVALID_NAME_LENGTH=Ungültige Namenslänge
INVALID_VALUE_LENGTH=Ungültige Wertlänge
INVALID_NAME_OWNER=Ungültiger Namensbesitzer
INVALID_BUYER=Ungültiger Käufer
INVALID_PUBLIC_KEY=Ungültiger Public Key
INVALID_OPTIONS_LENGTH=Ungültige Optionen-Länge
INVALID_OPTION_LENGTH=Ungültige Optionslänge
INVALID_DATA=Ungültige Daten
INVALID_DATA_LENGTH=Ungültige Datenlänge
INVALID_UPDATE_VALUE=Ungültiger Update-Wert
KEY_ALREADY_EXISTS=Der Schlüssel existiert bereits, Editieren ist deaktiviert
KEY_NOT_EXISTS=Der Schlüssel existiert nicht
LAST_KEY_IS_DEFAULT_KEY_ERROR=Du kannst den Schlüssel '${key}' nicht löschen, wenn er der einzige ist
FEE_LESS_REQUIRED=fee less required
WALLET_NOT_IN_SYNC=Das Wallet muss synchronisiert werden
INVALID_NETWORK_ADDRESS=Ungültige Netzwerkadresse
WALLET_NO_EXISTS=Das Wallet existiert nicht
WALLET_ADDRESS_NO_EXISTS=Die Adresse existiert nicht im Wallet
WALLET_LOCKED=Das Wallet ist abgeschlossen
WALLET_ALREADY_EXISTS=Das Wallet existiert bereits
WALLET_API_CALL_FORBIDDEN_BY_USER=Der Benutzer hat den API-Aufruf abgelehnt
BLOCK_NO_EXISTS=Der Block existiert nicht
TRANSACTION_NO_EXISTS=Die Transaktion existiert nicht
PUBLIC_KEY_NOT_FOUND=Public Key wurde nicht gefunden
NAME_NO_EXISTS=Der Name existiert nicht
NAME_ALREADY_EXISTS=Der Name existiert bereits
NAME_ALREADY_FOR_SALE=Der Name steht bereits zum Verkauf
NAME_NOT_LOWER_CASE=Der Name muss aus Kleinbuchstaben bestehen
NAME_SALE_NO_EXISTS=Namensverkauf existiert nicht
BUYER_ALREADY_OWNER=Der Käufer ist bereits Besitzer
POLL_NO_EXISTS=Die Abstimmung existiert nicht
POLL_ALREADY_EXISTS=Die Abstimmung existiert bereits
DUPLICATE_OPTION=Nicht alle Optionen sind eindeutig
POLL_OPTION_NO_EXISTS=Die option existiert nicht
ALREADY_VOTED_FOR_THAT_OPTION=Bereits für diese Option abgestimmt
INVALID_ASSET_ID=Ungültige Asset ID
NAME_NOT_REGISTERED=?NAME_NOT_REGISTERED?
NAME_FOR_SALE=?NAME_FOR_SALE?
NAME_WITH_SPACE=?NAME_WITH_SPACE?
INVALID_DESC_LENGTH=Ungültige Beschreibungslänge. Max. Länge ${MAX_LENGTH}
EMPTY_CODE=Der Code ist leer
DATA_SIZE=Ungültige Datenlänge
NULL_PAGES=Ungültige Seiten
INVALID_TYPE_LENGTH=Ungültige Typlänge
INVALID_TAGS_LENGTH=Ungültige Tag-Länge
INVALID_CREATION_BYTES=Fehler in Creation Bytes
BODY_EMPTY=invalid body it must not be empty
BLOG_DISABLED=Dieser Blog ist deaktiviert
NAME_NOT_OWNER=the creator address does not own the author name
TX_AMOUNT=the data size is too large - currently only ${BATCH_TX_AMOUNT} arbitrary transactions are allowed at once!
BLOG_ENTRY_NO_EXISTS=transaction with this signature contains no entries!
BLOG_EMPTY=this blog is empty
POSTID_EMPTY=the attribute postid is empty! this is the signature of the post you want to comment
POST_NOT_EXISTING=for the given postid no blogpost to comment was found
COMMENTING_DISABLED=commenting is for this blog disabled
COMMENT_NOT_EXISTING=for the given signature no comment was found
INVALID_COMMENT_OWNER=invalid comment owner
MESSAGE_FORMAT_NOT_HEX=the Message format is not hex - correct the text or use isTextMessage = true
MESSAGE_BLANK=The message attribute is missing or content is blank
NO_PUBLIC_KEY=The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to him.
MESSAGESIZE_EXCEEDED=Message size exceeded!
INVALID_ADDRESS = ung\u00FCltige adresse
INVALID_ASSET_ID = ung\u00FCltige asset ID
INVALID_DATA = ung\u00FCltige daten
INVALID_PUBLIC_KEY = ung\u00FCltiger public key
INVALID_SIGNATURE = ung\u00FCltige signatur
JSON = JSON nachricht konnte nicht geparsed werden
PUBLIC_KEY_NOT_FOUND = public key wurde nicht gefunden

View File

@@ -1,100 +1,57 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# Keys are from api.ApiError enum
# Common
UNKNOWN=unknown error
JSON=failed to parse json message
NO_BALANCE=not enough balance
NOT_YET_RELEASED=that feature is not yet released
UNAUTHORIZED=api call unauthorized
REPOSITORY_ISSUE=repository error
NON_PRODUCTION=This API call is not permitted for production systems
ADDRESS_UNKNOWN = account address unknown
# Validation
INVALID_SIGNATURE=invalid signature
INVALID_ADDRESS=invalid address
INVALID_SEED=invalid seed
INVALID_AMOUNT=invalid amount
INVALID_FEE=invalid fee
INVALID_SENDER=invalid sender
INVALID_RECIPIENT=invalid recipient
INVALID_NAME_LENGTH=invalid name length
INVALID_VALUE_LENGTH=invalid value length
INVALID_NAME_OWNER=invalid name owner
INVALID_BUYER=invalid buyer
INVALID_PUBLIC_KEY=invalid public key
INVALID_OPTIONS_LENGTH=invalid options length
INVALID_OPTION_LENGTH=invalid option length
INVALID_DATA=invalid data
INVALID_DATA_LENGTH=invalid data length
INVALID_UPDATE_VALUE=invalid update value
KEY_ALREADY_EXISTS=key already exists, edit is false
KEY_NOT_EXISTS=the key does not exist
FEE_LESS_REQUIRED=fee less required
WALLET_NOT_IN_SYNC=wallet needs to be synchronized
INVALID_NETWORK_ADDRESS=invalid network address
ADDRESS_NO_EXISTS=account address does not exist
INVALID_CRITERIA=invalid search criteria
INVALID_REFERENCE=invalid reference
INVALID_PRIVATE_KEY=invalid private key
INVALID_HEIGHT=invalid block height
# Wallet
WALLET_NO_EXISTS=wallet does not exist
WALLET_ADDRESS_NO_EXISTS=address does not exist in wallet
WALLET_LOCKED=wallet is locked
WALLET_ALREADY_EXISTS=wallet already exists
WALLET_API_CALL_FORBIDDEN_BY_USER=wallet denied api call
BLOCKCHAIN_NEEDS_SYNC = blockchain needs to synchronize first
# Blocks
BLOCK_NO_EXISTS=block does not exist
BLOCK_UNKNOWN = block unknown
# Transactions
TRANSACTION_NO_EXISTS=transaction does not exist
PUBLIC_KEY_NOT_FOUND=public key not found
TRANSACTION_INVALID=transaction invalid: %s
CANNOT_MINT = account cannot mint
# Names
NAME_NO_EXISTS=name does not exist
NAME_ALREADY_EXISTS=name already exists
NAME_ALREADY_FOR_SALE=name already for sale
NAME_NOT_LOWER_CASE=name must be lower case
NAME_SALE_NO_EXISTS=namesale does not exist
BUYER_ALREADY_OWNER=buyer is already owner
GROUP_UNKNOWN = group unknown
# Voting
POLL_NO_EXISTS=poll does not exist
POLL_ALREADY_EXISTS=poll already exists
DUPLICATE_OPTION=not all options are unique
POLL_OPTION_NO_EXISTS=option does not exist
ALREADY_VOTED_FOR_THAT_OPTION=already voted for that option
INVALID_ADDRESS = invalid address
# Assets
INVALID_ASSET_ID=invalid asset id
INVALID_ORDER_ID=invalid asset order id
ORDER_NO_EXISTS=unknown asset order id
INVALID_ASSET_ID = invalid asset id
# ATs
EMPTY_CODE=code is empty
DATA_SIZE=invalid data length
NULL_PAGES=invalid pages
INVALID_TYPE_LENGTH=invalid type length
INVALID_TAGS_LENGTH=invalid tags length
INVALID_CREATION_BYTES=error in creation bytes
INVALID_CRITERIA = invalid search criteria
# Blogs/Name-storage
BODY_EMPTY=invalid body it must not be empty
BLOG_DISABLED=this blog is disabled
NAME_NOT_OWNER=the creator address does not own the author name
BLOG_ENTRY_NO_EXISTS=transaction with this signature contains no entries!
BLOG_EMPTY=this blog is empty
POSTID_EMPTY=the attribute postid is empty! this is the signature of the post you want to comment
POST_NOT_EXISTING=for the given postid no blogpost to comment was found
COMMENTING_DISABLED=commenting is for this blog disabled
COMMENT_NOT_EXISTING=for the given signature no comment was found
INVALID_COMMENT_OWNER=invalid comment owner
INVALID_DATA = invalid data
# Messages
MESSAGE_FORMAT_NOT_HEX=the Message format is not hex - correct the text or use isTextMessage = true
MESSAGE_BLANK=The message attribute is missing or content is blank
NO_PUBLIC_KEY=The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to them.
MESSAGESIZE_EXCEEDED=Message size exceeded!
INVALID_HEIGHT = invalid block height
INVALID_NETWORK_ADDRESS = invalid network address
INVALID_ORDER_ID = invalid asset order ID
INVALID_PRIVATE_KEY = invalid private key
INVALID_PUBLIC_KEY = invalid public key
INVALID_REFERENCE = invalid reference
# Validation
INVALID_SIGNATURE = invalid signature
JSON = failed to parse json message
NAME_UNKNOWN = name unknown
NON_PRODUCTION = this API call is not permitted for production systems
ORDER_UNKNOWN = unknown asset order ID
PUBLIC_KEY_NOT_FOUND = public key not found
REPOSITORY_ISSUE = repository error
TRANSACTION_INVALID = transaction invalid: %s (%s)
TRANSACTION_UNKNOWN = transaction unknown
TRANSFORMATION_ERROR = could not transform JSON into transaction
UNAUTHORIZED = API call unauthorized

View File

@@ -1,100 +0,0 @@
# Keys are from api.ApiError enum
# Common
UNKNOWN=unknown error
JSON=failed to parse json message
NO_BALANCE=not enough balance
NOT_YET_RELEASED=that feature is not yet released
UNAUTHORIZED=api call unauthorized
REPOSITORY_ISSUE=repository error
NON_PRODUCTION=This API call is not permitted for production systems
# Validation
INVALID_SIGNATURE=invalid signature
INVALID_ADDRESS=invalid address
INVALID_SEED=invalid seed
INVALID_AMOUNT=invalid amount
INVALID_FEE=invalid fee
INVALID_SENDER=invalid sender
INVALID_RECIPIENT=invalid recipient
INVALID_NAME_LENGTH=invalid name length
INVALID_VALUE_LENGTH=invalid value length
INVALID_NAME_OWNER=invalid name owner
INVALID_BUYER=invalid buyer
INVALID_PUBLIC_KEY=invalid public key
INVALID_OPTIONS_LENGTH=invalid options length
INVALID_OPTION_LENGTH=invalid option length
INVALID_DATA=invalid data
INVALID_DATA_LENGTH=invalid data length
INVALID_UPDATE_VALUE=invalid update value
KEY_ALREADY_EXISTS=key already exists, edit is false
KEY_NOT_EXISTS=the key does not exist
FEE_LESS_REQUIRED=fee less required
WALLET_NOT_IN_SYNC=wallet needs to be synchronized
INVALID_NETWORK_ADDRESS=invalid network address
ADDRESS_NO_EXISTS=account address does not exist
INVALID_CRITERIA=invalid search criteria
INVALID_REFERENCE=invalid reference
INVALID_PRIVATE_KEY=invalid private key
INVALID_HEIGHT=invalid block height
# Wallet
WALLET_NO_EXISTS=wallet does not exist
WALLET_ADDRESS_NO_EXISTS=address does not exist in wallet
WALLET_LOCKED=wallet is locked
WALLET_ALREADY_EXISTS=wallet already exists
WALLET_API_CALL_FORBIDDEN_BY_USER=wallet denied api call
# Blocks
BLOCK_NO_EXISTS=block does not exist
# Transactions
TRANSACTION_NO_EXISTS=transaction does not exist
PUBLIC_KEY_NOT_FOUND=public key not found
TRANSACTION_INVALID=transaction invalid: %s
# Names
NAME_NO_EXISTS=name does not exist
NAME_ALREADY_EXISTS=name already exists
NAME_ALREADY_FOR_SALE=name already for sale
NAME_NOT_LOWER_CASE=name must be lower case
NAME_SALE_NO_EXISTS=namesale does not exist
BUYER_ALREADY_OWNER=buyer is already owner
# Voting
POLL_NO_EXISTS=poll does not exist
POLL_ALREADY_EXISTS=poll already exists
DUPLICATE_OPTION=not all options are unique
POLL_OPTION_NO_EXISTS=option does not exist
ALREADY_VOTED_FOR_THAT_OPTION=already voted for that option
# Assets
INVALID_ASSET_ID=invalid asset id
INVALID_ORDER_ID=invalid asset order id
ORDER_NO_EXISTS=unknown asset order id
# ATs
EMPTY_CODE=code is empty
DATA_SIZE=invalid data length
NULL_PAGES=invalid pages
INVALID_TYPE_LENGTH=invalid type length
INVALID_TAGS_LENGTH=invalid tags length
INVALID_CREATION_BYTES=error in creation bytes
# Blogs/Name-storage
BODY_EMPTY=invalid body it must not be empty
BLOG_DISABLED=this blog is disabled
NAME_NOT_OWNER=the creator address does not own the author name
BLOG_ENTRY_NO_EXISTS=transaction with this signature contains no entries!
BLOG_EMPTY=this blog is empty
POSTID_EMPTY=the attribute postid is empty! this is the signature of the post you want to comment
POST_NOT_EXISTING=for the given postid no blogpost to comment was found
COMMENTING_DISABLED=commenting is for this blog disabled
COMMENT_NOT_EXISTING=for the given signature no comment was found
INVALID_COMMENT_OWNER=invalid comment owner
# Messages
MESSAGE_FORMAT_NOT_HEX=the Message format is not hex - correct the text or use isTextMessage = true
MESSAGE_BLANK=The message attribute is missing or content is blank
NO_PUBLIC_KEY=The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to them.
MESSAGESIZE_EXCEEDED=Message size exceeded!

View File

@@ -1,6 +1,10 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = Applying automatic update and restarting...
AUTO_UPDATE = Auto Update
BLOCK_HEIGHT = height
CHECK_TIME_ACCURACY = Check time accuracy
@@ -9,6 +13,10 @@ CONNECTION = connection
CONNECTIONS = connections
CREATING_BACKUP_OF_DB_FILES = Creating backup of database files...
DB_BACKUP = Database Backup
EXIT = Exit
MINTING_DISABLED = NOT minting
@@ -22,8 +30,10 @@ NTP_NAG_TEXT_UNIX = Install NTP service to get an accurate clock.
NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
OPEN_NODE_UI = Open Node UI
OPEN_UI = Open UI
SYNCHRONIZE_CLOCK = Synchronize clock
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
SYNCHRONIZING_CLOCK = Synchronizing clock

View File

@@ -22,8 +22,10 @@ NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7
NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002
OPEN_NODE_UI = \u5F00\u542F\u754C\u9762
OPEN_UI = \u5F00\u542F\u754C\u9762
SYNCHRONIZE_CLOCK = \u540C\u6B65\u65F6\u949F
SYNCHRONIZING_BLOCKCHAIN = \u540C\u6B65\u533A\u5757\u94FE
SYNCHRONIZING_CLOCK = \u540C\u6B65\u7740\u65F6\u949F

View File

@@ -1,42 +1,176 @@
OK=OK
INVALID_ADDRESS=INVALID_ADDRESS
NEGATIVE_AMOUNT=NEGATIVE_AMOUNT
NEGATIVE_FEE=NEGATIVE_FEE
NO_BALANCE=NO_BALANCE
INVALID_REFERENCE=INVALID_REFERENCE
INVALID_NAME_LENGTH=INVALID_NAME_LENGTH
INVALID_VALUE_LENGTH=INVALID_VALUE_LENGTH
NAME_ALREADY_REGISTERED=NAME_ALREADY_REGISTERED
NAME_DOES_NOT_EXIST=NAME_DOES_NOT_EXIST
INVALID_NAME_OWNER=INVALID_NAME_OWNER
NAME_ALREADY_FOR_SALE=NAME_ALREADY_FOR_SALE
NAME_NOT_FOR_SALE=NAME_NOT_FOR_SALE
BUYER_ALREADY_OWNER=BUYER_ALREADY_OWNER
INVALID_AMOUNT=INVALID_AMOUNT
INVALID_SELLER=INVALID_SELLER
NAME_NOT_LOWER_CASE=NAME_NOT_LOWER_CASE
INVALID_DESCRIPTION_LENGTH=INVALID_DESCRIPTION_LENGTH
INVALID_OPTIONS_COUNT=INVALID_OPTIONS_COUNT
INVALID_OPTION_LENGTH=INVALID_OPTION_LENGTH
DUPLICATE_OPTION=DUPLICATE_OPTION
POLL_ALREADY_EXISTS=POLL_ALREADY_EXISTS
POLL_DOES_NOT_EXIST=POLL_DOES_NOT_EXIST
POLL_OPTION_DOES_NOT_EXIST=POLL_OPTION_DOES_NOT_EXIST
ALREADY_VOTED_FOR_THAT_OPTION=ALREADY_VOTED_FOR_THAT_OPTION
INVALID_DATA_LENGTH=INVALID_DATA_LENGTH
INVALID_QUANTITY=INVALID_QUANTITY
ASSET_DOES_NOT_EXIST=ASSET_DOES_NOT_EXIST
INVALID_RETURN=INVALID_RETURN
HAVE_EQUALS_WANT=HAVE_EQUALS_WANT
ORDER_DOES_NOT_EXIST=ORDER_DOES_NOT_EXIST
INVALID_ORDER_CREATOR=INVALID_ORDER_CREATOR
INVALID_PAYMENTS_COUNT=INVALID_PAYMENTS_COUNT
NEGATIVE_PRICE=NEGATIVE_PRICE
INVALID_CREATION_BYTES=INVALID_CREATION_BYTES
INVALID_TAGS_LENGTH=INVALID_TAGS_LENGTH
INVALID_AT_TYPE_LENGTH=INVALID_AT_TYPE_LENGTH
INVALID_AT_TRANSACTION=INVALID_AT_TRANSACTION
AT_IS_FINISHED=AT_IS_FINISHED
ASSET_DOES_NOT_MATCH_AT=ASSET_DOES_NOT_MATCH_AT
ASSET_ALREADY_EXISTS=ASSET_ALREADY_EXISTS
NOT_YET_RELEASED=NOT_YET_RELEASED
ACCOUNT_ALREADY_EXISTS = account already exists
ACCOUNT_CANNOT_REWARD_SHARE = account cannot reward-share
ALREADY_GROUP_ADMIN = already group admin
ALREADY_GROUP_MEMBER = already group member
ALREADY_VOTED_FOR_THAT_OPTION = already voted for that option
ASSET_ALREADY_EXISTS = asset already exists
ASSET_DOES_NOT_EXIST = ASSET_DOES_NOT_EXIST
ASSET_DOES_NOT_MATCH_AT = ASSET_DOES_NOT_MATCH_AT
ASSET_NOT_SPENDABLE = ASSET_NOT_SPENDABLE
AT_ALREADY_EXISTS = AT_ALREADY_EXISTS
AT_IS_FINISHED = AT_IS_FINISHED
AT_UNKNOWN = AT_UNKNOWN
BANNED_FROM_GROUP = BANNED_FROM_GROUP
BAN_EXISTS = BAN_EXISTS
BAN_UNKNOWN = BAN_UNKNOWN
BUYER_ALREADY_OWNER = BUYER_ALREADY_OWNER
CLOCK_NOT_SYNCED = CLOCK_NOT_SYNCED
DUPLICATE_OPTION = DUPLICATE_OPTION
GROUP_ALREADY_EXISTS = GROUP_ALREADY_EXISTS
GROUP_APPROVAL_DECIDED = GROUP_APPROVAL_DECIDED
GROUP_APPROVAL_NOT_REQUIRED = GROUP_APPROVAL_NOT_REQUIRED
GROUP_DOES_NOT_EXIST = GROUP_DOES_NOT_EXIST
GROUP_ID_MISMATCH = GROUP_ID_MISMATCH
GROUP_OWNER_CANNOT_LEAVE = GROUP_OWNER_CANNOT_LEAVE
HAVE_EQUALS_WANT = HAVE_EQUALS_WANT
INSUFFICIENT_FEE = INSUFFICIENT_FEE
INVALID_ADDRESS = INVALID_ADDRESS
INVALID_AMOUNT = INVALID_AMOUNT
INVALID_ASSET_OWNER = INVALID_ASSET_OWNER
INVALID_AT_TRANSACTION = INVALID_AT_TRANSACTION
INVALID_AT_TYPE_LENGTH = INVALID_AT_TYPE_LENGTH
INVALID_CREATION_BYTES = INVALID_CREATION_BYTES
INVALID_DATA_LENGTH = INVALID_DATA_LENGTH
INVALID_DESCRIPTION_LENGTH = INVALID_DESCRIPTION_LENGTH
INVALID_GROUP_APPROVAL_THRESHOLD = INVALID_GROUP_APPROVAL_THRESHOLD
INVALID_GROUP_ID = INVALID_GROUP_ID
INVALID_GROUP_OWNER = INVALID_GROUP_OWNER
INVALID_LIFETIME = INVALID_LIFETIME
INVALID_NAME_LENGTH = INVALID_NAME_LENGTH
INVALID_NAME_OWNER = INVALID_NAME_OWNER
INVALID_OPTIONS_COUNT = INVALID_OPTIONS_COUNT
INVALID_OPTION_LENGTH = INVALID_OPTION_LENGTH
INVALID_ORDER_CREATOR = INVALID_ORDER_CREATOR
INVALID_PAYMENTS_COUNT = INVALID_PAYMENTS_COUNT
INVALID_PUBLIC_KEY = INVALID_PUBLIC_KEY
INVALID_QUANTITY = INVALID_QUANTITY
INVALID_REFERENCE = INVALID_REFERENCE
INVALID_RETURN = INVALID_RETURN
INVALID_REWARD_SHARE_PERCENT = INVALID_REWARD_SHARE_PERCENT
INVALID_SELLER = INVALID_SELLER
INVALID_TAGS_LENGTH = INVALID_TAGS_LENGTH
INVALID_TX_GROUP_ID = INVALID_TX_GROUP_ID
INVALID_VALUE_LENGTH = INVALID_VALUE_LENGTH
INVITE_UNKNOWN = INVITE_UNKNOWN
JOIN_REQUEST_EXISTS = JOIN_REQUEST_EXISTS
MAXIMUM_REWARD_SHARES = MAXIMUM_REWARD_SHARES
MISSING_CREATOR = MISSING_CREATOR
MULTIPLE_NAMES_FORBIDDEN = MULTIPLE_NAMES_FORBIDDEN
NAME_ALREADY_FOR_SALE = NAME_ALREADY_FOR_SALE
NAME_ALREADY_REGISTERED = NAME_ALREADY_REGISTERED
NAME_DOES_NOT_EXIST = NAME_DOES_NOT_EXIST
NAME_NOT_FOR_SALE = NAME_NOT_FOR_SALE
NAME_NOT_LOWER_CASE = NAME_NOT_LOWER_CASE
NEGATIVE_AMOUNT = NEGATIVE_AMOUNT
NEGATIVE_FEE = NEGATIVE_FEE
NEGATIVE_PRICE = NEGATIVE_PRICE
NOT_GROUP_ADMIN = NOT_GROUP_ADMIN
NOT_GROUP_MEMBER = NOT_GROUP_MEMBER
NOT_MINTING_ACCOUNT = NOT_MINTING_ACCOUNT
NOT_YET_RELEASED = NOT_YET_RELEASED
NO_BALANCE = NO_BALANCE
NO_BLOCKCHAIN_LOCK = node's blockchain currently busy
NO_FLAG_PERMISSION = NO_FLAG_PERMISSION
OK = OK
ORDER_ALREADY_CLOSED = ORDER_ALREADY_CLOSED
ORDER_DOES_NOT_EXIST = ORDER_DOES_NOT_EXIST
POLL_ALREADY_EXISTS = POLL_ALREADY_EXISTS
POLL_DOES_NOT_EXIST = POLL_DOES_NOT_EXIST
POLL_OPTION_DOES_NOT_EXIST = POLL_OPTION_DOES_NOT_EXIST
PUBLIC_KEY_UNKNOWN = PUBLIC_KEY_UNKNOWN
SELF_SHARE_EXISTS = SELF_SHARE_EXISTS
TIMESTAMP_TOO_NEW = TIMESTAMP_TOO_NEW
TIMESTAMP_TOO_OLD = TIMESTAMP_TOO_OLD
TOO_MANY_UNCONFIRMED = TOO_MANY_UNCONFIRMED
TRANSACTION_ALREADY_CONFIRMED = TRANSACTION_ALREADY_CONFIRMED
TRANSACTION_ALREADY_EXISTS = TRANSACTION_ALREADY_EXISTS
TRANSACTION_UNKNOWN = TRANSACTION_UNKNOWN
TX_GROUP_ID_MISMATCH = TX_GROUP_ID_MISMATCH

View File

@@ -1 +0,0 @@
Node UI goes here!

View File

@@ -3,6 +3,7 @@ package org.qortal.test;
import org.junit.Test;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.settings.Settings;
import org.qortal.test.common.Common;
import org.qortal.transaction.CreateAssetOrderTransaction;
import org.qortal.transaction.CreatePollTransaction;
@@ -22,7 +23,7 @@ public class CompatibilityTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useSettings("test-settings-v1.json");
NTP.testMode();
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
}
@Test

View File

@@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
public class EPCTests {
@@ -60,13 +61,10 @@ public class EPCTests {
ScheduledExecutorService statusExecutor = Executors.newSingleThreadScheduledExecutor();
statusExecutor.scheduleAtFixedRate(() -> {
synchronized (testEPC) {
final long seconds = (System.currentTimeMillis() - start) / 1000L;
System.out.println(String.format("After %d second%s, active threads: %d, greatest thread count: %d, tasks produced: %d, tasks consumed: %d",
seconds, (seconds != 1 ? "s" : ""),
testEPC.getActiveThreadCount(), testEPC.getGreatestActiveThreadCount(),
testEPC.getTasksProduced(), testEPC.getTasksConsumed()));
}
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
final long seconds = (System.currentTimeMillis() - start) / 1000L;
System.out.print(String.format("After %d second%s, ", seconds, (seconds != 1 ? "s" : "")));
printSnapshot(snapshot);
}, 1L, 1L, TimeUnit.SECONDS);
// Let it run for a minute
@@ -78,10 +76,17 @@ public class EPCTests {
final long after = System.currentTimeMillis();
System.out.println(String.format("Shutdown took %d milliseconds", after - before));
System.out.println(String.format("Greatest thread count: %d", testEPC.getGreatestActiveThreadCount()));
System.out.println(String.format("Tasks produced: %d", testEPC.getTasksProduced()));
System.out.println(String.format("Tasks consumed: %d", testEPC.getTasksConsumed()));
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
System.out.print("After shutdown, ");
printSnapshot(snapshot);
}
private void printSnapshot(final StatsSnapshot snapshot) {
System.out.println(String.format("threads: %d active (%d max, %d exhaustion%s), tasks: %d produced / %d consumed",
snapshot.activeThreadCount, snapshot.greatestActiveThreadCount,
snapshot.spawnFailures, (snapshot.spawnFailures != 1 ? "s": ""),
snapshot.tasksProduced, snapshot.tasksConsumed));
}
@Test

View File

@@ -12,6 +12,7 @@ import org.qortal.transaction.Transaction;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.Serialization;
import com.google.common.hash.HashCode;
@@ -19,6 +20,9 @@ import io.druid.extendedset.intset.ConciseSet;
import static org.junit.Assert.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
@@ -142,4 +146,30 @@ public class SerializationTests extends Common {
}
}
@Test
public void testPositiveBigDecimal() throws IOException {
BigDecimal amount = new BigDecimal("123.4567").setScale(8);
byte[] bytes = Serialization.serializeBigDecimal(amount);
assertEquals("Serialized BigDecimal should be 8 bytes long", 8, bytes.length);
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
BigDecimal newAmount = Serialization.deserializeBigDecimal(byteBuffer);
assertEqualBigDecimals("Deserialized BigDecimal has incorrect value", amount, newAmount);
}
@Test
public void testNegativeBigDecimal() throws IOException {
BigDecimal amount = new BigDecimal("-1.23").setScale(8);
byte[] bytes = Serialization.serializeBigDecimal(amount);
assertEquals("Serialized BigDecimal should be 8 bytes long", 8, bytes.length);
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
BigDecimal newAmount = Serialization.deserializeBigDecimal(byteBuffer);
assertEqualBigDecimals("Deserialized BigDecimal has incorrect value", amount, newAmount);
}
}

View File

@@ -5,6 +5,7 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockMinter;
import org.qortal.data.account.AccountData;
@@ -19,11 +20,13 @@ import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transform.Transformer;
import static org.junit.Assert.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Random;
public class TransferPrivsTests extends Common {
@@ -42,6 +45,27 @@ public class TransferPrivsTests extends Common {
Common.orphanCheck();
}
@Test
public void testAliceIntoNewAccountTransferPrivs() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
assertTrue(alice.canMint());
PrivateKeyAccount aliceMintingAccount = Common.getTestAccount(repository, "alice-reward-share");
byte[] randomPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
Random random = new Random();
random.nextBytes(randomPublicKey);
Account randomAccount = new PublicKeyAccount(repository, randomPublicKey);
combineAccounts(repository, alice, randomAccount, aliceMintingAccount);
assertFalse(alice.canMint());
assertTrue(randomAccount.canMint());
}
}
@Test
public void testAliceIntoDilbertTransferPrivs() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {

View File

@@ -94,7 +94,7 @@ public class AssetsApiTests extends ApiCommon {
try {
assertNotNull(this.assetsResource.getAssetOrder(FAKE_ORDER_ID_BASE58));
} catch (ApiException e) {
assertTrue(e.error == ApiError.ORDER_NO_EXISTS.getCode());
assertTrue(e.error == ApiError.ORDER_UNKNOWN.getCode());
}
}
@@ -103,13 +103,13 @@ public class AssetsApiTests extends ApiCommon {
try {
assertNotNull(this.assetsResource.getAssetOrderTrades(FAKE_ORDER_ID_BASE58, null, null, null));
} catch (ApiException e) {
assertTrue(e.error == ApiError.ORDER_NO_EXISTS.getCode());
assertTrue(e.error == ApiError.ORDER_UNKNOWN.getCode());
}
try {
assertNotNull(this.assetsResource.getAssetOrderTrades(FAKE_ORDER_ID_BASE58, 1, 1, true));
} catch (ApiException e) {
assertTrue(e.error == ApiError.ORDER_NO_EXISTS.getCode());
assertTrue(e.error == ApiError.ORDER_UNKNOWN.getCode());
}
}

View File

@@ -0,0 +1,60 @@
package org.qortal.test.apps;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import org.qortal.api.ApiError;
import org.qortal.globalization.Translator;
import org.qortal.transaction.Transaction.ValidationResult;
public class CheckTranslations {
private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" };
private static final Set<String> SYSTRAY_KEYS = Set.of("AUTO_UPDATE", "APPLYING_UPDATE_AND_RESTARTING", "BLOCK_HEIGHT",
"CHECK_TIME_ACCURACY", "CONNECTION", "CONNECTIONS", "CREATING_BACKUP_OF_DB_FILES", "DB_BACKUP", "EXIT",
"MINTING_DISABLED", "MINTING_ENABLED", "NTP_NAG_CAPTION", "NTP_NAG_TEXT_UNIX", "NTP_NAG_TEXT_WINDOWS",
"OPEN_UI", "SYNCHRONIZE_CLOCK", "SYNCHRONIZING_BLOCKCHAIN", "SYNCHRONIZING_CLOCK");
private static String failurePrefix;
public static void main(String[] args) {
for (String lang : SUPPORTED_LANGS) {
System.out.println(String.format("\n# Checking '%s' translations", lang));
Locale.setDefault(Locale.forLanguageTag(lang));
failurePrefix = "!!" + lang + ":";
checkTranslations("TransactionValidity", lang, Arrays.stream(ValidationResult.values()).map(value -> value.name()).collect(Collectors.toSet()));
checkTranslations("ApiError", lang, Arrays.stream(ApiError.values()).map(value -> value.name()).collect(Collectors.toSet()));
checkTranslations("SysTray", lang, SYSTRAY_KEYS);
}
}
private static void checkTranslations(String className, String lang, Set<String> keys) {
System.out.println(String.format("## Checking '%s' translations for %s", lang, className));
Set<String> allKeys = Translator.INSTANCE.keySet(className, lang);
if (allKeys == null) {
System.out.println(String.format("NO '%s' translations for %s!", lang, className));
allKeys = Collections.emptySet();
}
for (String key : keys) {
allKeys.remove(key);
String translation = Translator.INSTANCE.translate(className, lang, key);
if (translation.startsWith(failurePrefix))
System.out.println(String.format("Missing key '%s' in %s_%s.properties", key, className, lang));
}
// Any leftover keys?
for (String key : allKeys)
System.out.println(String.format("Extraneous key '%s' in %s_%s.properties", key, className, lang));
}
}

View File

@@ -1,200 +1,44 @@
package org.qortal.test.apps;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Executors;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.NtpV3Packet;
import org.apache.commons.net.ntp.TimeInfo;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.LoggerContext;
import org.qortal.utils.NTP;
public class NTPTests {
private static final List<String> CC_TLDS = Arrays.asList("oceania", "europe", "cn", "asia", "africa");
public static void main(String[] args) throws UnknownHostException, IOException, InterruptedException {
NTPUDPClient client = new NTPUDPClient();
client.setDefaultTimeout(2000);
class NTPServer {
private static final int MIN_POLL = 8;
public char usage = ' ';
public String remote;
public String refId;
public Integer stratum;
public char type = 'u'; // unicast
public int poll = MIN_POLL;
public byte reach = 0;
public Long delay;
public Double offset;
public Double jitter;
private Deque<Double> offsets = new LinkedList<>();
private double totalSquareOffsets = 0.0;
private long nextPoll;
private Long lastGood;
public NTPServer(String remote) {
this.remote = remote;
}
public boolean poll(NTPUDPClient client) {
final long now = System.currentTimeMillis();
if (now < this.nextPoll)
return false;
boolean isUpdated = false;
try {
TimeInfo timeInfo = client.getTime(InetAddress.getByName(remote));
timeInfo.computeDetails();
NtpV3Packet ntpMessage = timeInfo.getMessage();
this.refId = ntpMessage.getReferenceIdString();
this.stratum = ntpMessage.getStratum();
this.poll = Math.max(MIN_POLL, 1 << ntpMessage.getPoll());
this.delay = timeInfo.getDelay();
this.offset = (double) timeInfo.getOffset();
if (this.offsets.size() == 8) {
double oldOffset = this.offsets.removeFirst();
this.totalSquareOffsets -= oldOffset * oldOffset;
}
this.offsets.addLast(this.offset);
this.totalSquareOffsets += this.offset * this.offset;
this.jitter = Math.sqrt(this.totalSquareOffsets / this.offsets.size());
this.reach = (byte) ((this.reach << 1) | 1);
this.lastGood = now;
isUpdated = true;
} catch (IOException e) {
this.reach <<= 1;
}
this.nextPoll = now + this.poll * 1000;
return isUpdated;
}
public Integer getWhen() {
if (this.lastGood == null)
return null;
return (int) ((System.currentTimeMillis() - this.lastGood) / 1000);
}
}
List<NTPServer> ntpServers = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
List<String> ntpServers = new ArrayList<>();
for (String ccTld : CC_TLDS)
for (int subpool = 0; subpool <=3; ++subpool)
ntpServers.add(new NTPServer(subpool + "." + ccTld + ".pool.ntp.org"));
for (int subpool = 0; subpool <= 3; ++subpool)
ntpServers.add(new String(subpool + "." + ccTld + ".pool.ntp.org"));
while (true) {
Thread.sleep(1000);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
NTP.shutdownNow();
}));
CompletionService<Boolean> ecs = new ExecutorCompletionService<Boolean>(Executors.newCachedThreadPool());
for (NTPServer server : ntpServers)
ecs.submit(() -> server.poll(client));
Logger ntpLogger = LogManager.getLogger(NTP.class);
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
Configuration config = loggerContext.getConfiguration();
LoggerConfig loggerConfig = config.getLoggerConfig(ntpLogger.getName());
boolean showReport = false;
for (int i = 0; i < ntpServers.size(); ++i)
try {
showReport = ecs.take().get() || showReport;
} catch (ExecutionException e) {
// skip
}
loggerConfig.setLevel(Level.TRACE);
loggerContext.updateLoggers(config);
if (showReport) {
double s0 = 0;
double s1 = 0;
double s2 = 0;
NTP.start(ntpServers.toArray(new String[0]));
for (NTPServer server : ntpServers) {
if (server.offset == null) {
server.usage = ' ';
continue;
}
server.usage = '+';
double value = server.offset * (double) server.stratum;
s0 += 1;
s1 += value;
s2 += value * value;
}
if (s0 < ntpServers.size() / 3 + 1) {
System.out.println("Not enough replies to calculate network time");
} else {
double filterStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
double filterMean = s1 / s0;
// Now only consider offsets within 1 stddev?
s0 = 0;
s1 = 0;
s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null || server.reach == 0)
continue;
if (Math.abs(server.offset * (double)server.stratum - filterMean) > filterStddev)
continue;
server.usage = '*';
s0 += 1;
s1 += server.offset;
s2 += server.offset * server.offset;
}
if (s0 <= 1) {
System.out.println(String.format("Not enough values to calculate network time. stddev: %7.4f", filterStddev));
} else {
double mean = s1 / s0;
double newStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
System.out.println(String.format("filtering stddev: %7.3f, mean: %7.3f, new stddev: %7.3f, nValues: %.0f / %d", filterStddev, mean, newStddev, s0, ntpServers.size()));
}
}
System.out.println(String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
));
for (NTPServer server : ntpServers)
System.out.println(String.format("%c%16.16s %16.16s %2s %c %4s %4d %3o %7s %7s %7s",
server.usage,
server.remote,
formatNull("%s", server.refId, ""),
formatNull("%2d", server.stratum, ""),
server.type,
formatNull("%4d", server.getWhen(), "-"),
server.poll,
server.reach,
formatNull("%5dms", server.delay, ""),
formatNull("% 5.0fms", server.offset, ""),
formatNull("%5.2fms", server.jitter, "")
));
}
}
}
private static String formatNull(String format, Object arg, String nullOutput) {
return arg != null ? String.format(format, arg) : nullOutput;
// Endless sleep
Thread.sleep(1000000000L);
}
}

View File

@@ -6,6 +6,7 @@ import org.junit.Test;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.test.common.AccountUtils;
import org.qortal.test.common.AssetUtils;
import org.qortal.test.common.Common;
@@ -19,7 +20,7 @@ public class OldTradingTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useSettings("test-settings-old-asset.json");
NTP.testMode();
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
}
@After

View File

@@ -116,7 +116,7 @@ public class Common {
public static void useDefaultSettings() throws DataException {
useSettings(testSettingsFilename);
NTP.testMode();
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
}
public static void resetBlockchain() throws DataException {

View File

@@ -4,9 +4,11 @@ import static org.junit.Assert.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.bitcoinj.core.Base58;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -204,4 +206,55 @@ public class RewardTests extends Common {
}
}
/** Test rewards to founders, one in reward-share, the other is self-share. */
@Test
public void testFounderRewards() throws DataException {
Common.useSettings("test-settings-v2-founder-rewards.json");
BigDecimal perHundred = BigDecimal.valueOf(100L);
try (final Repository repository = RepositoryManager.getRepository()) {
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
// Alice to mint, therefore online
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
mintingAndOnlineAccounts.add(aliceSelfShare);
// Bob self-share NOT online
// Chloe self-share and reward-share with Dilbert both online
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
mintingAndOnlineAccounts.add(chloeSelfShare);
PrivateKeyAccount chloeDilbertRewardShare = new PrivateKeyAccount(repository, Base58.decode("HuiyqLipUN1V9p1HZfLhyEwmEA6BTaT2qEfjgkwPViV4"));
mintingAndOnlineAccounts.add(chloeDilbertRewardShare);
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// 3 founders (online or not) so blockReward divided by 3
BigDecimal founderCount = BigDecimal.valueOf(3L);
BigDecimal perFounderReward = blockReward.divide(founderCount, RoundingMode.DOWN);
// Alice simple self-share so her reward is perFounderReward
AccountUtils.assertBalance(repository, "alice", Asset.QORT, perFounderReward);
// Bob not online so his reward is simply perFounderReward
AccountUtils.assertBalance(repository, "bob", Asset.QORT, perFounderReward);
// Chloe has two reward-shares, so her reward is divided by 2
BigDecimal chloeSharesCount = BigDecimal.valueOf(2L);
BigDecimal chloePerShareReward = perFounderReward.divide(chloeSharesCount, RoundingMode.DOWN);
// Her self-share gets chloePerShareReward
BigDecimal chloeExpectedBalance = chloePerShareReward;
// Her reward-share with Dilbert: 25% goes to Dilbert
BigDecimal dilbertSharePercent = BigDecimal.valueOf(25L);
BigDecimal dilbertExpectedBalance = chloePerShareReward.multiply(dilbertSharePercent).divide(perHundred, RoundingMode.DOWN);
// The remaining 75% goes to Chloe
BigDecimal rewardShareRemaining = chloePerShareReward.subtract(dilbertExpectedBalance);
chloeExpectedBalance = chloeExpectedBalance.add(rewardShareRemaining);
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeExpectedBalance);
}
}
}

View File

@@ -0,0 +1,63 @@
{
"isTestChain": true,
"blockTimestampMargin": 500,
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },
{ "height": 21, "reward": 1 }
],
"sharesByLevel": [
{ "levels": [ 1, 2 ], "share": 0.05 },
{ "levels": [ 3, 4 ], "share": 0.10 },
{ "levels": [ 5, 6 ], "share": 0.15 },
{ "levels": [ 7, 8 ], "share": 0.20 },
{ "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
],
"featureTriggers": {
"messageHeight": 0,
"atHeight": 0,
"assetsTimestamp": 0,
"votingTimestamp": 0,
"arbitraryTimestamp": 0,
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
},
"genesisInfo": {
"version": 4,
"timestamp": 0,
"transactions": [
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
{ "type": "ACCOUNT_FLAGS", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "REWARD_SHARE", "minterPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "rewardSharePublicKey": "CcABzvk26TFEHG7Yok84jxyd4oBtLkx8RJdGFVz2csvp", "sharePercent": 100 },
{ "type": "ACCOUNT_FLAGS", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "REWARD_SHARE", "minterPublicKey": "7KNBj2MnEb6zq1vvKY1q8G2Voctcc2Z1X4avFyEH2eJC", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "rewardSharePublicKey": "6bnEKqZbsCSWryUQnbBT9Umufdu3CapFvxfAni6afhFb", "sharePercent": 100 },
{ "type": "REWARD_SHARE", "minterPublicKey": "7KNBj2MnEb6zq1vvKY1q8G2Voctcc2Z1X4avFyEH2eJC", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "rewardSharePublicKey": "Hebh14YXUdJA66Vq8KyffNXHx3NSDUAZaNH9qbfEvf5M", "sharePercent": 25 }
]
}
}

View File

@@ -0,0 +1,6 @@
{
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json",
"wipeUnconfirmedOnStart": false,
"minPeers": 0
}

92
tools/approve-auto-update.sh Executable file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env bash
printf "Searching for auto-update transactions to approve...\n";
tx=$( curl --silent --url "http://localhost:12391/arbitrary/search?txGroupId=1&service=1&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
if fgrep --silent '"approvalStatus":"PENDING"' <<< "${tx}"; then
true
else
echo "Can't find any pending transactions"
exit
fi
sig=$( perl -n -e 'print $1 if m/"signature":"(\w+)"/' <<< "${tx}" )
if [ -z "${sig}" ]; then
printf "Can't find transaction signature in JSON:\n%s\n" "${tx}"
exit
fi
printf "Found transaction %s\n" $sig;
printf "\nPaste your dev account private key:\n";
IFS=
read -s privkey
printf "\n"
# Convert to public key
pubkey=$( curl --silent --url "http://localhost:12391/utils/publickey" --data @- <<< "${privkey}" );
if egrep -v --silent '^\w{44,46}$' <<< "${pubkey}"; then
printf "Invalid response from API - was your private key correct?\n%s\n" "${pubkey}"
exit
fi
printf "Your public key: %s\n" ${pubkey}
# Convert to address
address=$( curl --silent --url "http://localhost:12391/addresses/convert/${pubkey}" );
printf "Your address: %s\n" ${address}
# Grab last reference
lastref=$( curl --silent --url "http://localhost:12391/addresses/lastreference/{$address}" );
printf "Your last reference: %s\n" ${lastref}
# Build GROUP_APPROVAL transaction
timestamp=$( date +%s )000
tx_json=$( cat <<TX_END
{
"timestamp": ${timestamp},
"reference": "${lastref}",
"fee": 0.001,
"txGroupId": 0,
"adminPublicKey": "${pubkey}",
"pendingSignature": "${sig}",
"approval": true
}
TX_END
)
raw_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:12391/groups/approval" --data @- <<< "${tx_json}" )
if egrep -v --silent '^\w{100,}' <<< "${raw_tx}"; then
printf "Building GROUP_APPROVAL transaction failed:\n%s\n" "${raw_tx}"
exit
fi
printf "\nRaw approval tx:\n%s\n" ${raw_tx}
# sign
sign_json=$( cat <<SIGN_END
{
"privateKey": "${privkey}",
"transactionBytes": "${raw_tx}"
}
SIGN_END
)
signed_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:12391/transactions/sign" --data @- <<< "${sign_json}" )
printf "\nSigned tx:\n%s\n" ${signed_tx}
if egrep -v --silent '^\w{100,}' <<< "${signed_tx}"; then
printf "Signing GROUP_APPROVAL transaction failed:\n%s\n" "${signed_tx}"
exit
fi
# ready to publish?
plural="s"
printf "\n"
for ((seconds = 5; seconds > 0; seconds--)); do
if [ "${seconds}" = "1" ]; then
plural=""
fi
printf "\rBroadcasting in %d second%s...(CTRL-C) to abort " $seconds $plural
sleep 1
done
printf "\rBroadcasting signed GROUP_APPROVAL transaction... \n"
result=$( curl --silent --url "http://localhost:12391/transactions/process" --data @- <<< "${signed_tx}" )
printf "API response:\n%s\n" "${result}"

81
tools/build-auto-update.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -e
# Check we are within a git repo
git_dir=$( git rev-parse --show-toplevel )
if [ -z "${git_dir}" ]; then
echo "Cannot determine top-level directory for git repo"
exit 1
fi
# Change to git top-level
cd ${git_dir}
# Check we are in 'master' branch
branch_name=$( git symbolic-ref -q HEAD )
branch_name=${branch_name##refs/heads/}
echo "Current git branch: ${branch_name}"
if [ "${branch_name}" != "master" ]; then
echo "Unexpected current branch '${branch_name}' - expecting 'master'"
exit 1
fi
# Extract short-form commit hash
short_hash=$( git rev-parse --short HEAD )
if [ -z "${short_hash}" ]; then
echo "Unable to extract short-form current commit hash"
exit 1
fi
echo "HEAD commit is: ${short_hash}"
# Check there are no uncommitted changes
uncommitted=$( git status --short --untracked-files=no )
if [ ! -z "${uncommitted}" ]; then
echo "Cannot continue due to uncommitted files:"
echo "${uncommitted}"
exit 1
fi
# Determine project name
project=$( perl -n -e 'if (m/<artifactId>(\w+)<.artifactId>/) { print $1; exit }' pom.xml $)
if [ -z "${project}" ]; then
echo "Unable to determine project name from pom.xml?"
exit 1
fi
# Actually rebuild JAR
echo "Building ${project} JAR..."
mvn clean package 1>/tmp/${project}-mvn-build.log 2>&1
if [ "$?" != "0" -o ! -r target/${project}*.jar ]; then
echo "Maven build failed. For details, see /tmp/${project}-mvn-build.log"
exit 1
fi
# Convert packaged JAR to XORed update form
echo "Building ${project}.update..."
java -cp target/${project}*.jar org.qortal.XorUpdate target/${project}*.jar ${project}.update
if [ "$?" != "0" ]; then
echo "Failed to create XORed auto-update JAR"
exit 1
fi
# Create auto-update branch from this commit
update_branch=auto-update-${short_hash}
if git show-ref --quiet --verify refs/heads/${update_branch}; then
echo "Existing auto-update branch based on this commit (${short_hash}) - deleting..."
git branch --delete --force ${update_branch}
fi
echo "Checking out new auto-update branch based on this commit (${short_hash})..."
git checkout --orphan ${update_branch}
git rm --cached -fr . 1>/dev/null
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}
echo "Changing back to 'master' branch"
git checkout --force master

136
tools/publish-auto-update.pl Executable file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env perl
use strict;
use warnings;
use POSIX;
use Getopt::Std;
sub usage() {
die("usage: $0 [-p api-port] dev-private-key\n");
}
my %opt;
getopts('p:', \%opt);
usage() unless @ARGV == 1;
my $port = $opt{p} || 12391;
my $privkey = shift @ARGV;
my $git_dir = `git rev-parse --show-toplevel`;
die("Cannot determine git top level dir\n") unless $git_dir;
chomp $git_dir;
chdir($git_dir) || die("Can't change directory to $git_dir: $!\n");
open(POM, '<', 'pom.xml') || die ("Can't open 'pom.xml': $!\n");
my $project;
while (<POM>) {
if (m/<artifactId>(\w+)<.artifactId>/o) {
$project = $1;
last;
}
}
close(POM);
# short-form commit hash on 'master' branch
my $commit_hash = `git show --no-patch --format=%h`;
die("Can't find commit hash\n") if ! defined $commit_hash;
chomp $commit_hash;
printf "Commit hash on 'master' branch: %s\n", $commit_hash;
# build timestamp / commit timestamp on 'master' branch
my $timestamp = `git show --no-patch --format=%ct`;
die("Can't determine commit timestamp\n") if ! defined $timestamp;
$timestamp *= 1000; # Convert to milliseconds
# locate sha256 utility
my $SHA256 = `which sha256sum || which sha256`;
# SHA256 of actual update file
my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256}`;
die("Can't calculate SHA256 of ${project}.update\n") unless $sha256 =~ m/(\S{64})/;
chomp $sha256;
# long-form commit hash of HEAD on auto-update branch
my $update_hash = `git rev-parse refs/heads/auto-update-${commit_hash}`;
die("Can't find commit hash for HEAD on auto-update-${commit_hash} branch\n") if ! defined $update_hash;
chomp $update_hash;
printf "Build timestamp (ms): %d / 0x%016x\n", $timestamp, $timestamp;
printf "Auto-update commit hash: %s\n", $update_hash;
printf "SHA256 of ${project}.update: %s\n", $sha256;
my $tx_type = 10;
my $tx_timestamp = time() * 1000;
my $tx_group_id = 1;
my $service = 1;
printf "\nARBITRARY(%d) transaction with timestamp %d, txGroupID %d and service %d\n", $tx_type, $tx_timestamp, $tx_group_id, $service;
my $data_hex = sprintf "%016x%s%s", $timestamp, $update_hash, $sha256;
printf "\nARBITRARY transaction data payload: %s\n", $data_hex;
my $n_payments = 0;
my $is_raw = 1; # RAW_DATA
my $data_length = length($data_hex) / 2; # two hex chars per byte
my $fee = 0.001 * 1e8;
die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60;
my $pubkey = `curl --silent --url http://localhost:${port}/utils/publickey --data ${privkey}`;
die("Can't convert private key to public key:\n$pubkey\n") unless $pubkey =~ m/^\w{44}$/;
printf "\nPublic key: %s\n", $pubkey;
my $pubkey_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${pubkey}`;
die("Can't convert base58 public key to hex:\n$pubkey_hex\n") unless $pubkey_hex =~ m/^[A-Za-z0-9]{64}$/;
printf "Public key hex: %s\n", $pubkey_hex;
my $address = `curl --silent --url http://localhost:${port}/addresses/convert/${pubkey}`;
die("Can't convert base58 public key to address:\n$address\n") unless $address =~ m/^\w{34}$/;
printf "Address: %s\n", $address;
my $reference = `curl --silent --url http://localhost:${port}/addresses/lastreference/${address}`;
die("Can't fetch last reference for $address:\n$reference\n") unless $reference =~ m/^\w{88}$/;
printf "Last reference: %s\n", $reference;
my $reference_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${reference}`;
die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/;
printf "Last reference hex: %s\n", $reference_hex;
my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%02x%08x%s%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $n_payments, $service, $is_raw, $data_length, $data_hex, $fee);
printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex;
my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`;
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{255,265}$/; # Roughly 255 to 265 base58 chars
printf "\nRaw transaction (base58):\n%s\n", $raw_tx;
my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${raw_tx}" } '|;
my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`;
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{345,355}$/; # +90ish longer than $raw_tx
printf "\nSigned transaction:\n%s\n", $signed_tx;
# Check we can actually fetch update
my $origin = `git remote get-url origin`;
die("Unable to get github url for 'origin'?\n") unless $origin && $origin =~ m/:(.*)\.git$/;
my $repo = $1;
my $update_url = "https://github.com/${repo}/raw/${update_hash}/${project}.update";
my $fetch_result = `curl --silent -o /dev/null --location --range 0-1 --head --write-out '%{http_code}' --url ${update_url}`;
die("\nUnable to fetch update from ${update_url}\n") if $fetch_result ne '200';
printf "\nUpdate fetchable from ${update_url}\n";
# Flush STDOUT after every output
$| = 1;
print "\n";
for (my $delay = 5; $delay > 0; --$delay) {
printf "\rSubmitting transaction in %d second%s... CTRL-C to abort ", $delay, ($delay != 1 ? 's' : '');
sleep 1;
}
printf "\rSubmitting transaction NOW... \n";
my $result = `curl --silent --url http://localhost:${port}/transactions/process --data ${signed_tx}`;
chomp $result;
die("Transaction wasn't accepted:\n$result\n") unless $result eq 'true';
my $decoded_tx = `curl --silent -H "Content-Type: application/json" --url http://localhost:${port}/transactions/decode --data ${signed_tx}`;
printf "\nTransaction accepted:\n$decoded_tx\n";