Compare commits

..

13 Commits

Author SHA1 Message Date
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
20 changed files with 495 additions and 116 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

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

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

@@ -341,17 +341,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

@@ -231,6 +231,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()));

View File

@@ -1,5 +1,6 @@
package org.qortal.controller;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
@@ -99,7 +100,7 @@ public class Controller extends Thread {
private static final long MISBEHAVIOUR_COOLOFF = 10 * 60 * 1000L; // ms
private static final int MAX_BLOCKCHAIN_TIP_AGE = 5; // blocks
private static final Object shutdownLock = new Object();
private static final String repositoryUrlTemplate = "jdbc:hsqldb:file:%s/blockchain;create=true;hsqldb.full_log_replay=true";
private static final String repositoryUrlTemplate = "jdbc:hsqldb:file:%s" + File.separator + "blockchain;create=true;hsqldb.full_log_replay=true";
private static final long ARBITRARY_REQUEST_TIMEOUT = 5 * 1000L; // ms
private static final long REPOSITORY_BACKUP_PERIOD = 123 * 60 * 1000L; // ms
private static final long NTP_PRE_SYNC_CHECK_PERIOD = 5 * 1000L; // ms
@@ -240,6 +241,10 @@ public class Controller extends Thread {
return this.savedArgs;
}
/* package */ static boolean isStopping() {
return isStopping;
}
// Entry point
public static void main(String[] args) {
@@ -310,6 +315,7 @@ public class Controller extends Thread {
network.start();
} catch (IOException e) {
LOGGER.error("Unable to start networking", e);
Controller.getInstance().shutdown();
Gui.getInstance().fatalError("Networking failure", e);
return; // Not System.exit() so that GUI can display error
}
@@ -343,6 +349,7 @@ public class Controller extends Thread {
apiService.start();
} catch (Exception e) {
LOGGER.error("Unable to start API", e);
Controller.getInstance().shutdown();
Gui.getInstance().fatalError("API failure", e);
return; // Not System.exit() so that GUI can display error
}
@@ -542,6 +549,10 @@ public class Controller extends Thread {
LOGGER.debug(() -> String.format("Failed to synchronize with peer %s (%s)", peer, syncResult.name()));
break;
case SHUTTING_DOWN:
// Just quietly exit
break;
case OK:
requestSysTrayUpdate = true;
// fall-through...
@@ -1331,7 +1342,7 @@ public class Controller extends Thread {
}
}
public void ensureTestingAccountOnline(PrivateKeyAccount mintingAccount) {
public void ensureTestingAccountsOnline(PrivateKeyAccount... onlineAccounts) {
if (!BlockChain.getInstance().isTestChain()) {
LOGGER.warn("Ignoring attempt to ensure test account is online for non-test chain!");
return;
@@ -1341,19 +1352,21 @@ public class Controller extends Thread {
if (now == null)
return;
// Check mintingAccount is actually reward-share?
// Add reward-share & timestamp to online accounts
final long onlineAccountsTimestamp = Controller.toOnlineAccountTimestamp(now);
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
byte[] signature = mintingAccount.sign(timestampBytes);
byte[] publicKey = mintingAccount.getPublicKey();
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
synchronized (this.onlineAccounts) {
this.onlineAccounts.clear();
this.onlineAccounts.add(ourOnlineAccountData);
for (PrivateKeyAccount onlineAccount : onlineAccounts) {
// Check mintingAccount is actually reward-share?
byte[] signature = onlineAccount.sign(timestampBytes);
byte[] publicKey = onlineAccount.getPublicKey();
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
this.onlineAccounts.add(ourOnlineAccountData);
}
}
}

View File

@@ -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
@@ -94,6 +94,9 @@ public class Synchronizer {
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
List<BlockSummaryData> peerBlockSummaries = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight);
if (peerBlockSummaries == null && Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
if (peerBlockSummaries == null) {
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
return SynchronizationResult.NO_REPLY;
@@ -154,6 +157,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 +218,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 +241,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;
@@ -331,6 +343,10 @@ public class Synchronizer {
BlockData testBlockData = null;
while (testHeight >= 1) {
// Are we shutting down?
if (Controller.isStopping())
return null;
// Fetch our block signature at this height
testBlockData = repository.getBlockRepository().fromHeight(testHeight);
if (testBlockData == null) {

View File

@@ -88,7 +88,10 @@ 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"
};
public static final int MAX_SIGNATURES_PER_REPLY = 500;
@@ -478,18 +481,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);

View File

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

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

@@ -7,6 +7,7 @@ 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 +145,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 +173,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 +287,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 +496,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 +564,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;
}
}
@@ -768,6 +846,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
// Minting accounts used by BlockMinter
@Override
public List<MintingAccountData> getMintingAccounts() throws DataException {
List<MintingAccountData> mintingAccounts = new ArrayList<>();
@@ -787,6 +866,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void save(MintingAccountData mintingAccountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("MintingAccounts");
@@ -799,6 +879,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 +890,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 +944,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void save(QortFromQoraData qortFromQoraData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountQortFromQoraInfo");
@@ -841,6 +959,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public int deleteQortFromQoraInfo(String address) throws DataException {
try {
return this.repository.delete("AccountQortFromQoraInfo", "account = ?", address);

View File

@@ -928,6 +928,11 @@ 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;
default:
// nothing to do
return false;

View File

@@ -337,8 +337,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 +361,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) {

View File

@@ -77,11 +77,11 @@ public class Settings {
/** 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 = 40;
/** Maximum number of peer connections we allow. */
private int maxPeers = 50;
private int maxPeers = 80;
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources

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
}