Browse Source

Rework distributing block reward to legacy qora holders

Fix related unit tests.

Fix assetID support in GENESIS transactions.
pull/67/head
catbref 5 years ago
parent
commit
ebc2ee6ea9
  1. 2
      src/main/java/org/qora/asset/Asset.java
  2. 135
      src/main/java/org/qora/block/Block.java
  3. 9
      src/main/java/org/qora/block/BlockChain.java
  4. 2
      src/main/java/org/qora/block/BlockMinter.java
  5. 52
      src/main/java/org/qora/data/account/QortFromQoraData.java
  6. 7
      src/main/java/org/qora/repository/AccountRepository.java
  7. 35
      src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java
  8. 6
      src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
  9. 3
      src/main/java/org/qora/transaction/GenesisTransaction.java
  10. 10
      src/test/java/org/qora/test/common/BlockUtils.java
  11. 16
      src/test/java/org/qora/test/common/Common.java
  12. 55
      src/test/java/org/qora/test/minting/RewardTests.java
  13. 69
      src/test/resources/test-chain-v2-qora-holder.json
  14. 22
      src/test/resources/test-chain-v2.json
  15. 6
      src/test/resources/test-settings-v2-qora-holder.json

2
src/main/java/org/qora/asset/Asset.java

@ -16,6 +16,8 @@ public class Asset {
/** Hard-coded asset representing legacy QORA held in old QORA1 blockchain. */
public static final long LEGACY_QORA = 1L;
/** Hard-coded asset representing QORT gained from holding legacy QORA. */
public static final long QORT_FROM_QORA = 2L;
// Other useful constants

135
src/main/java/org/qora/block/Block.java

@ -8,7 +8,9 @@ import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@ -28,6 +30,7 @@ import org.qora.controller.Controller;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.account.QortFromQoraData;
import org.qora.data.account.RewardShareData;
import org.qora.data.at.ATData;
import org.qora.data.at.ATStateData;
@ -37,6 +40,7 @@ import org.qora.data.block.BlockTransactionData;
import org.qora.data.network.OnlineAccountData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.ATRepository;
import org.qora.repository.AccountRepository.BalanceOrdering;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.TransactionRepository;
@ -131,7 +135,6 @@ public class Block {
final Account mintingAccount;
final AccountData mintingAccountData;
final boolean isMinterFounder;
final BigDecimal minterQoraAmount;
final int shareBin;
final Account recipientAccount;
@ -146,12 +149,6 @@ public class Block {
this.mintingAccount = new PublicKeyAccount(repository, this.rewardShareData.getMinterPublicKey());
this.recipientAccount = new Account(repository, this.rewardShareData.getRecipient());
AccountBalanceData qoraBalanceData = repository.getAccountRepository().getBalance(this.mintingAccount.getAddress(), Asset.LEGACY_QORA);
if (qoraBalanceData != null && qoraBalanceData.getBalance() != null && qoraBalanceData.getBalance().compareTo(BigDecimal.ZERO) > 0)
this.minterQoraAmount = qoraBalanceData.getBalance();
else
this.minterQoraAmount = null;
this.mintingAccountData = repository.getAccountRepository().getAccount(this.mintingAccount.getAddress());
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
@ -1261,7 +1258,7 @@ public class Block {
if (reward == null)
return;
distributeByAccountLevel(reward);
distributeBlockReward(reward);
}
protected void processTransactions() throws DataException {
@ -1344,7 +1341,7 @@ public class Block {
if (blockFees.compareTo(BigDecimal.ZERO) <= 0)
return;
distributeByAccountLevel(blockFees);
distributeBlockReward(blockFees);
}
protected void processAtFeesAndStates() throws DataException {
@ -1486,7 +1483,7 @@ public class Block {
if (reward == null)
return;
distributeByAccountLevel(reward.negate());
distributeBlockReward(reward.negate());
}
protected void deductTransactionFees() throws DataException {
@ -1496,7 +1493,7 @@ public class Block {
if (blockFees.compareTo(BigDecimal.ZERO) <= 0)
return;
distributeByAccountLevel(blockFees.negate());
distributeBlockReward(blockFees.negate());
}
protected void orphanAtFeesAndStates() throws DataException {
@ -1562,7 +1559,9 @@ public class Block {
}
}
protected void distributeByAccountLevel(BigDecimal totalAmount) throws DataException {
protected void distributeBlockReward(BigDecimal totalAmount) throws DataException {
LOGGER.trace(() -> String.format("Distributing: %s", totalAmount.toPlainString()));
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
@ -1593,33 +1592,111 @@ public class Block {
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<ExpandedAccount> qoraHolderAccounts = new ArrayList<>();
BigDecimal totalQoraHeld = BigDecimal.ZERO;
for (int i = 0; i < expandedAccounts.size(); ++i) {
ExpandedAccount expandedAccount = expandedAccounts.get(i);
if (expandedAccount.minterQoraAmount == null)
continue;
List<String> assetAddresses = Collections.emptyList();
List<Long> assetIds = Collections.singletonList(Asset.LEGACY_QORA);
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getAssetBalances(assetAddresses, assetIds, BalanceOrdering.ASSET_ACCOUNT, true, null, null, null);
qoraHolderAccounts.add(expandedAccount);
totalQoraHeld = totalQoraHeld.add(expandedAccount.minterQoraAmount);
// 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 whose final block is earlier than this one
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
if (qortFromQoraData == null)
throw new IllegalStateException(String.format("Missing QORT-from-QORA data for %s", qoraHolder.getAddress()));
if (qortFromQoraData.getFinalBlockHeight() != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight())
qoraHoldersIterator.remove();
}
}
final BigDecimal finalTotalQoraHeld = totalQoraHeld;
BigDecimal totalQoraHeld = BigDecimal.ZERO;
for (int i = 0; i < qoraHolders.size(); ++i)
totalQoraHeld = totalQoraHeld.add(qoraHolders.get(i).getBalance());
BigDecimal finalTotalQoraHeld = totalQoraHeld;
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", finalTotalQoraHeld.toPlainString()));
for (int h = 0; h < qoraHolderAccounts.size(); ++h) {
ExpandedAccount expandedAccount = qoraHolderAccounts.get(h);
final BigDecimal holderAmount = qoraHoldersAmount.multiply(totalQoraHeld).divide(expandedAccount.minterQoraAmount, RoundingMode.DOWN);
LOGGER.trace(() -> String.format("Minter account %s has %s / %s QORA so share: %s",
expandedAccount.mintingAccount.getAddress(), expandedAccount.minterQoraAmount, finalTotalQoraHeld, holderAmount.toPlainString()));
for (int h = 0; h < qoraHolders.size(); ++h) {
AccountBalanceData qoraHolder = qoraHolders.get(h);
final BigDecimal holderReward = qoraHoldersAmount.multiply(totalQoraHeld).divide(qoraHolder.getBalance(), RoundingMode.DOWN).setScale(8, RoundingMode.DOWN);
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, holderReward.toPlainString()));
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
if (qortFromQoraData == null)
qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), BigDecimal.ZERO.setScale(8), null);
BigDecimal qortFromQora = holderReward.divide(qoraPerQortReward, RoundingMode.DOWN);
BigDecimal newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA).add(qortFromQora);
// If processing, make sure we don't overpay
if (totalAmount.signum() >= 0) {
BigDecimal maxQortFromQora = qoraHolder.getBalance().divide(qoraPerQortReward, RoundingMode.DOWN);
if (newQortFromQoraBalance.compareTo(maxQortFromQora) >= 0) {
// Reduce final QORT-from-QORA payment to match max
BigDecimal adjustment = newQortFromQoraBalance.subtract(maxQortFromQora);
qortFromQora = qortFromQora.subtract(adjustment);
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
// This is also qora holders final qort-from-qora block
qortFromQoraData.setFinalQortFromQora(qortFromQora);
qortFromQoraData.setFinalBlockHeight(this.blockData.getHeight());
BigDecimal finalQortFromQora = qortFromQora;
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight()));
}
} else {
// Orphaning
if (qortFromQoraData.getFinalBlockHeight() != 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 = qortFromQora.subtract(qortFromQoraData.getFinalQortFromQora().negate());
qortFromQora = qortFromQora.subtract(adjustment);
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
qortFromQoraData.setFinalQortFromQora(null);
qortFromQoraData.setFinalBlockHeight(null);
BigDecimal finalQortFromQora = qortFromQora;
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight()));
}
}
qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(qortFromQora));
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
this.repository.getAccountRepository().save(qortFromQoraData);
expandedAccount.distribute(holderAmount);
sharedAmount = sharedAmount.add(holderAmount);
sharedAmount = sharedAmount.add(holderReward);
}
// Spread remainder across founder accounts
BigDecimal foundersAmount = totalAmount.subtract(sharedAmount);
LOGGER.debug(String.format("Shared %s of %s, remaining %s to founders", sharedAmount.toPlainString(), totalAmount.toPlainString(), foundersAmount.toPlainString()));
BigDecimal finalSharedAmount = sharedAmount;
LOGGER.debug(() -> String.format("Shared %s of %s, remaining %s to founders", finalSharedAmount.toPlainString(), totalAmount.toPlainString(), foundersAmount.toPlainString()));
List<ExpandedAccount> founderAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isMinterFounder).collect(Collectors.toList());
if (founderAccounts.isEmpty())

9
src/main/java/org/qora/block/BlockChain.java

@ -109,6 +109,8 @@ public class BlockChain {
/** Share of block reward/fees to legacy QORA coin holders */
BigDecimal qoraHoldersShare;
/** How many legacy QORA per 1 QORT of block reward. */
BigDecimal qoraPerQortReward;
/**
* Number of minted blocks required to reach next level from previous.
@ -312,6 +314,10 @@ public class BlockChain {
return this.qoraHoldersShare;
}
public BigDecimal getQoraPerQortReward() {
return this.qoraPerQortReward;
}
public int getMinAccountLevelToMint() {
return this.minAccountLevelToMint;
}
@ -403,6 +409,9 @@ public class BlockChain {
if (this.qoraHoldersShare == null)
Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config");
if (this.qoraPerQortReward == null)
Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config");
if (this.blocksNeededByLevel == null)
Settings.throwValidationError("No \"blocksNeededByLevel\" entry found in blockchain config");

2
src/main/java/org/qora/block/BlockMinter.java

@ -360,6 +360,8 @@ public class BlockMinter extends Thread {
// Add to blockchain
newBlock.process();
LOGGER.info(String.format("Minted new test block: %d", newBlock.getBlockData().getHeight()));
repository.saveChanges();
} finally {
blockchainLock.unlock();

52
src/main/java/org/qora/data/account/QortFromQoraData.java

@ -0,0 +1,52 @@
package org.qora.data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class QortFromQoraData {
// Properties
private String address;
// Not always present:
private BigDecimal finalQortFromQora;
private Integer finalBlockHeight;
// Constructors
// necessary for JAXB
protected QortFromQoraData() {
}
public QortFromQoraData(String address, BigDecimal finalQortFromQora, Integer finalBlockHeight) {
this.address = address;
this.finalQortFromQora = finalQortFromQora;
this.finalBlockHeight = finalBlockHeight;
}
// Getters/Setters
public String getAddress() {
return this.address;
}
public BigDecimal getFinalQortFromQora() {
return this.finalQortFromQora;
}
public void setFinalQortFromQora(BigDecimal finalQortFromQora) {
this.finalQortFromQora = finalQortFromQora;
}
public Integer getFinalBlockHeight() {
return this.finalBlockHeight;
}
public void setFinalBlockHeight(Integer finalBlockHeight) {
this.finalBlockHeight = finalBlockHeight;
}
}

7
src/main/java/org/qora/repository/AccountRepository.java

@ -5,6 +5,7 @@ import java.util.List;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.account.MintingAccountData;
import org.qora.data.account.QortFromQoraData;
import org.qora.data.account.RewardShareData;
public interface AccountRepository {
@ -138,4 +139,10 @@ public interface AccountRepository {
/** Delete minting account info, used by BlockMinter, from repository using passed private key. */
public int delete(byte[] mintingAccountPrivateKey) throws DataException;
// Managing QORT from legacy QORA
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException;
public void save(QortFromQoraData qortFromQoraData) throws DataException;
}

35
src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java

@ -10,6 +10,7 @@ import java.util.List;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.account.MintingAccountData;
import org.qora.data.account.QortFromQoraData;
import org.qora.data.account.RewardShareData;
import org.qora.repository.AccountRepository;
import org.qora.repository.DataException;
@ -660,4 +661,38 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
// Managing QORT from legacy QORA
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException {
String sql = "SELECT final_qort_from_qora, final_block_height FROM AccountQortFromQoraInfo WHERE account = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
return null;
BigDecimal finalQortFromQora = resultSet.getBigDecimal(1);
Integer finalBlockHeight = resultSet.getInt(2);
if (finalBlockHeight == 0 && resultSet.wasNull())
finalBlockHeight = null;
return new QortFromQoraData(address, finalQortFromQora, finalBlockHeight);
} catch (SQLException e) {
throw new DataException("Unable to fetch account qort-from-qora info from repository", e);
}
}
public void save(QortFromQoraData qortFromQoraData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountQortFromQoraInfo");
saveHelper.bind("account", qortFromQoraData.getAddress())
.bind("final_qort_from_qora", qortFromQoraData.getFinalQortFromQora())
.bind("final_block_height", qortFromQoraData.getFinalBlockHeight());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save account qort-from-qora info into repository", e);
}
}
}

6
src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java

@ -841,6 +841,12 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("DROP INDEX BlockGenerationHeightIndex");
break;
case 59:
// Keeping track of QORT gained from holding legacy QORA
stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QoraAddress, final_qort_from_qora QoraAmount, final_block_height INT, "
+ "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
break;
default:
// nothing to do
return false;

3
src/main/java/org/qora/transaction/GenesisTransaction.java

@ -7,7 +7,6 @@ import java.util.List;
import org.qora.account.Account;
import org.qora.account.PrivateKeyAccount;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.transaction.GenesisTransactionData;
import org.qora.data.transaction.TransactionData;
@ -139,7 +138,7 @@ public class GenesisTransaction extends Transaction {
Account recipient = new Account(repository, genesisTransactionData.getRecipient());
// Update recipient's balance
recipient.setConfirmedBalance(Asset.QORT, genesisTransactionData.getAmount());
recipient.setConfirmedBalance(genesisTransactionData.getAssetId(), genesisTransactionData.getAmount());
}
@Override

10
src/test/java/org/qora/test/common/BlockUtils.java

@ -2,6 +2,8 @@ package org.qora.test.common;
import java.math.BigDecimal;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.Block;
import org.qora.block.BlockChain;
@ -12,6 +14,8 @@ import org.qora.repository.Repository;
public class BlockUtils {
private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class);
/** Mints a new block using "alice-reward-share" test account. */
public static void mintBlock(Repository repository) throws DataException {
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share");
@ -26,8 +30,14 @@ public class BlockUtils {
public static void orphanLastBlock(Repository repository) throws DataException {
BlockData blockData = repository.getBlockRepository().getLastBlock();
final int height = blockData.getHeight();
Block block = new Block(repository, blockData);
block.orphan();
LOGGER.info(String.format("Orphaned block: %d", height));
repository.saveChanges();
}

16
src/test/java/org/qora/test/common/Common.java

@ -16,16 +16,13 @@ import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Base58;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.qora.block.Block;
import org.qora.block.BlockChain;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.asset.AssetData;
import org.qora.data.block.BlockData;
import org.qora.data.group.GroupData;
import org.qora.repository.AccountRepository.BalanceOrdering;
import org.qora.repository.DataException;
@ -65,11 +62,6 @@ public class Common {
private static List<GroupData> initialGroups;
private static List<AccountBalanceData> initialBalances;
// TODO: converts users of these constants to TestAccount schema
public static final byte[] v2testPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
public static final byte[] v2testPublicKey = Base58.decode("2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP");
public static final String v2testAddress = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v";
private static Map<String, TestAccount> testAccountsByName = new HashMap<>();
static {
testAccountsByName.put("alice", new TestAccount(null, "alice", "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6", false));
@ -131,10 +123,7 @@ public class Common {
try (final Repository repository = RepositoryManager.getRepository()) {
// Orphan back to genesis block
while (repository.getBlockRepository().getBlockchainHeight() > 1) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
Block block = new Block(repository, blockData);
block.orphan();
repository.saveChanges();
BlockUtils.orphanLastBlock(repository);
}
List<AssetData> remainingAssets = repository.getAssetRepository().getAllAssets();
@ -172,6 +161,9 @@ public class Common {
List<T> remainingClone = new ArrayList<T>(remaining);
remainingClone.removeIf(isInitial);
for (T remainingEntry : remainingClone)
LOGGER.info(String.format("Non-genesis remaining entry: %s", keyExtractor.apply(remainingEntry)));
assertTrue(String.format("Non-genesis %s remains", typeName), remainingClone.isEmpty());
}

55
src/test/java/org/qora/test/minting/RewardTests.java

@ -48,23 +48,24 @@ public class RewardTests extends Common {
@Test
public void testRewards() throws DataException {
List<RewardByHeight> rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight();
try (final Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
List<RewardByHeight> rewards = BlockChain.getInstance().getBlockRewardsByHeight();
int rewardIndex = rewards.size() - 1;
int rewardIndex = rewardsByHeight.size() - 1;
RewardByHeight rewardInfo = rewards.get(rewardIndex);
RewardByHeight rewardInfo = rewardsByHeight.get(rewardIndex);
BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORT);
for (int height = rewardInfo.height; height > 1; --height) {
if (height < rewardInfo.height) {
--rewardIndex;
rewardInfo = rewards.get(rewardIndex);
rewardInfo = rewardsByHeight.get(rewardIndex);
}
BlockUtils.mintBlock(repository);
expectedBalance = expectedBalance.add(rewardInfo.reward);
}
@ -82,6 +83,7 @@ public class RewardTests extends Common {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
BlockMinter.mintTestingBlock(repository, rewardShareAccount);
// We're expecting reward * 12.8% to Bob, the rest to Alice
@ -94,4 +96,47 @@ public class RewardTests extends Common {
}
}
@Test
public void testLegacyQoraReward() throws DataException {
Common.useSettings("test-settings-v2-qora-holder.json");
BigDecimal qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
BigDecimal qoraPerQort = BlockChain.getInstance().getQoraPerQortReward();
try (final Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.QORT_FROM_QORA);
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
BlockUtils.mintBlock(repository);
// Expected reward
BigDecimal expectedReward = blockReward.multiply(qoraHoldersShare).divide(qoraPerQort, RoundingMode.DOWN);
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT).add(expectedReward));
AccountUtils.assertBalance(repository, "chloe", Asset.QORT_FROM_QORA, initialBalances.get("chloe").get(Asset.QORT_FROM_QORA).add(expectedReward));
}
}
@Test
public void testMaxLegacyQoraReward() throws DataException {
Common.useSettings("test-settings-v2-qora-holder.json");
BigDecimal qoraPerQort = BlockChain.getInstance().getQoraPerQortReward();
try (final Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
// Mint lots of blocks
for (int i = 0; i < 100; ++i)
BlockUtils.mintBlock(repository);
// Expected balances to be limited by Chloe's legacy QORA amount
BigDecimal expectedBalance = initialBalances.get("chloe").get(Asset.LEGACY_QORA).divide(qoraPerQort);
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT).add(expectedBalance));
AccountUtils.assertBalance(repository, "chloe", Asset.QORT_FROM_QORA, initialBalances.get("chloe").get(Asset.QORT_FROM_QORA).add(expectedBalance));
}
}
}

69
src/test/resources/test-chain-v2-qora-holder.json

@ -0,0 +1,69 @@
{
"isTestChain": true,
"blockTimestampMargin": 500,
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"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,
"v2Timestamp": 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": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "100", "assetId": 1 },
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "owner": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }
]
}
}

22
src/test/resources/test-chain-v2.json

@ -23,6 +23,7 @@
{ "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 }
@ -42,18 +43,25 @@
"version": 4,
"timestamp": 0,
"transactions": [
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "data": "", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000", "fee": 0 },
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000", "fee": 0 },
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000", "fee": 0 },
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000", "fee": 0 },
{ "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": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "owner": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 },
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 }
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }
]
}
}

6
src/test/resources/test-settings-v2-qora-holder.json

@ -0,0 +1,6 @@
{
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json",
"wipeUnconfirmedOnStart": false,
"minPeers": 0
}
Loading…
Cancel
Save