diff --git a/src/main/java/org/qora/asset/Asset.java b/src/main/java/org/qora/asset/Asset.java index 5afbd122..381ef933 100644 --- a/src/main/java/org/qora/asset/Asset.java +++ b/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 diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 1f00d537..48b68109 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/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 sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel(); List 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 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 assetAddresses = Collections.emptyList(); + List assetIds = Collections.singletonList(Asset.LEGACY_QORA); + List 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 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 founderAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isMinterFounder).collect(Collectors.toList()); if (founderAccounts.isEmpty()) diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index c704a83c..7aa39165 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/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"); diff --git a/src/main/java/org/qora/block/BlockMinter.java b/src/main/java/org/qora/block/BlockMinter.java index 6cbeaedc..5abf30c0 100644 --- a/src/main/java/org/qora/block/BlockMinter.java +++ b/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(); diff --git a/src/main/java/org/qora/data/account/QortFromQoraData.java b/src/main/java/org/qora/data/account/QortFromQoraData.java new file mode 100644 index 00000000..fec69f4d --- /dev/null +++ b/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; + } + +} diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index aaf43533..31370cb8 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/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; + } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java index 753f0e7b..25a7dd01 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/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); + } + } + } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index 5b63ebc8..aa82a22f 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/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; diff --git a/src/main/java/org/qora/transaction/GenesisTransaction.java b/src/main/java/org/qora/transaction/GenesisTransaction.java index 53d40fc0..c60c62db 100644 --- a/src/main/java/org/qora/transaction/GenesisTransaction.java +++ b/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 diff --git a/src/test/java/org/qora/test/common/BlockUtils.java b/src/test/java/org/qora/test/common/BlockUtils.java index 27fa4e30..a2507349 100644 --- a/src/test/java/org/qora/test/common/BlockUtils.java +++ b/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(); } diff --git a/src/test/java/org/qora/test/common/Common.java b/src/test/java/org/qora/test/common/Common.java index 123d3e64..198b763c 100644 --- a/src/test/java/org/qora/test/common/Common.java +++ b/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 initialGroups; private static List 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 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 remainingAssets = repository.getAssetRepository().getAllAssets(); @@ -172,6 +161,9 @@ public class Common { List remainingClone = new ArrayList(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()); } diff --git a/src/test/java/org/qora/test/minting/RewardTests.java b/src/test/java/org/qora/test/minting/RewardTests.java index 690b0d7c..8104f8ed 100644 --- a/src/test/java/org/qora/test/minting/RewardTests.java +++ b/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 rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight(); + try (final Repository repository = RepositoryManager.getRepository()) { Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT); - List 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> 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> 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> 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)); + } + } + } \ No newline at end of file diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json new file mode 100644 index 00000000..4cee7dc2 --- /dev/null +++ b/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 } + ] + } +} diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index fb6de970..9b4f36bd 100644 --- a/src/test/resources/test-chain-v2.json +++ b/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 } ] } } diff --git a/src/test/resources/test-settings-v2-qora-holder.json b/src/test/resources/test-settings-v2-qora-holder.json new file mode 100644 index 00000000..0248c867 --- /dev/null +++ b/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 +}