From 31cbc1f15b3e3c4235fbc01f315a9c76e2dd0452 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 7 Nov 2019 15:53:37 +0000 Subject: [PATCH] Account assets balances now height-dependant. QORT-from-QORA block reward fixes. --- src/main/java/org/qora/account/Account.java | 2 +- src/main/java/org/qora/block/Block.java | 52 +++-- .../qora/repository/AccountRepository.java | 10 + .../hsqldb/HSQLDBAccountRepository.java | 71 ++++++- .../hsqldb/HSQLDBDatabaseUpdates.java | 17 ++ .../org/qora/test/AccountBalanceTests.java | 180 ++++++++++++++++++ .../org/qora/test/minting/RewardTests.java | 40 +++- .../resources/test-chain-v2-qora-holder.json | 3 +- 8 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 src/test/java/org/qora/test/AccountBalanceTests.java diff --git a/src/main/java/org/qora/account/Account.java b/src/main/java/org/qora/account/Account.java index e7739557..d5ede3d2 100644 --- a/src/main/java/org/qora/account/Account.java +++ b/src/main/java/org/qora/account/Account.java @@ -163,7 +163,7 @@ public class Account { * @throws DataException */ public void setLastReference(byte[] reference) throws DataException { - LOGGER.trace(() -> String.format("Setting last reference for %s to %s", this.address, Base58.encode(reference))); + LOGGER.trace(() -> String.format("Setting last reference for %s to %s", this.address, (reference == null ? "null" : Base58.encode(reference)))); AccountData accountData = this.buildAccountData(); accountData.setReference(reference); diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 423b4e28..b68983ad 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -1250,7 +1250,7 @@ public class Block { accountData.setBlocksMinted(accountData.getBlocksMinted() + 1); repository.getAccountRepository().setMintedBlockCount(accountData); - LOGGER.trace(() -> String.format("Block minted %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""))); + LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""))); } // We are only interested in accounts that are NOT founders and NOT already highest level @@ -1437,6 +1437,9 @@ public class Block { // Return AT fees and delete AT states from repository orphanAtFeesAndStates(); + // Delete orphaned balances + this.repository.getAccountRepository().deleteBalancesFromHeight(this.blockData.getHeight()); + // Delete block from blockchain this.repository.getBlockRepository().delete(this.blockData); this.blockData.setHeight(null); @@ -1640,12 +1643,9 @@ public class Block { qoraHoldersIterator.remove(); } else { // We're orphaning a block - // so disregard qora holders whose final block is earlier than this one + // 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) - throw new IllegalStateException(String.format("Missing QORT-from-QORA data for %s", qoraHolder.getAddress())); - - if (qortFromQoraData.getFinalBlockHeight() != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight()) + if (qortFromQoraData != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight()) qoraHoldersIterator.remove(); } } @@ -1660,18 +1660,14 @@ public class Block { 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); + BigDecimal holderReward = qoraHoldersAmount.multiply(qoraHolder.getBalance()).divide(totalQoraHeld, RoundingMode.DOWN).setScale(8, RoundingMode.DOWN); + BigDecimal finalHolderReward = holderReward; LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s", - qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, holderReward.toPlainString())); + qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, finalHolderReward.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); + BigDecimal newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA).add(holderReward); // If processing, make sure we don't overpay if (totalAmount.signum() >= 0) { @@ -1681,41 +1677,39 @@ public class Block { // Reduce final QORT-from-QORA payment to match max BigDecimal adjustment = newQortFromQoraBalance.subtract(maxQortFromQora); - qortFromQora = qortFromQora.subtract(adjustment); + holderReward = holderReward.subtract(adjustment); newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment); // This is also qora holders final qort-from-qora block - qortFromQoraData.setFinalQortFromQora(qortFromQora); - qortFromQoraData.setFinalBlockHeight(this.blockData.getHeight()); + QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, this.blockData.getHeight()); + this.repository.getAccountRepository().save(qortFromQoraData); - BigDecimal finalQortFromQora = qortFromQora; + BigDecimal finalAdjustedHolderReward = holderReward; LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d", - qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight())); + qoraHolder.getAddress(), finalAdjustedHolderReward.toPlainString(), this.blockData.getHeight())); } } else { // Orphaning - if (qortFromQoraData.getFinalBlockHeight() != null) { + 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 = qortFromQora.subtract(qortFromQoraData.getFinalQortFromQora().negate()); + BigDecimal adjustment = holderReward.subtract(qortFromQoraData.getFinalQortFromQora().negate()); - qortFromQora = qortFromQora.subtract(adjustment); + holderReward = holderReward.subtract(adjustment); newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment); - qortFromQoraData.setFinalQortFromQora(null); - qortFromQoraData.setFinalBlockHeight(null); + this.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress()); - BigDecimal finalQortFromQora = qortFromQora; + BigDecimal finalAdjustedHolderReward = holderReward; LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d", - qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight())); + qoraHolder.getAddress(), finalAdjustedHolderReward.toPlainString(), this.blockData.getHeight())); } } - qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(qortFromQora)); + qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(holderReward)); qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance); - this.repository.getAccountRepository().save(qortFromQoraData); - sharedAmount = sharedAmount.add(holderReward); } diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index 31370cb8..19e920c8 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/src/main/java/org/qora/repository/AccountRepository.java @@ -86,18 +86,26 @@ public interface AccountRepository { public AccountBalanceData getBalance(String address, long assetId) throws DataException; + /** Returns account balance data for address & assetId at (or before) passed block height. */ + public AccountBalanceData getBalance(String address, long assetId, int height) throws DataException; + public enum BalanceOrdering { ASSET_BALANCE_ACCOUNT, ACCOUNT_ASSET, ASSET_ACCOUNT } + public List getAssetBalances(long assetId, Boolean excludeZero) throws DataException; + public List getAssetBalances(List addresses, List assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException; public void save(AccountBalanceData accountBalanceData) throws DataException; public void delete(String address, long assetId) throws DataException; + /** Deletes orphaned balances at block height >= height. */ + public int deleteBalancesFromHeight(int height) throws DataException; + // Reward-shares public RewardShareData getRewardShare(byte[] mintingAccountPublicKey, String recipientAccount) throws DataException; @@ -145,4 +153,6 @@ public interface AccountRepository { public void save(QortFromQoraData qortFromQoraData) throws DataException; + public int deleteQortFromQoraInfo(String address) 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 25a7dd01..f05ca721 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java @@ -259,7 +259,7 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public AccountBalanceData getBalance(String address, long assetId) throws DataException { - String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ?"; + String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? ORDER BY height DESC LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId)) { if (resultSet == null) @@ -273,6 +273,48 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public AccountBalanceData getBalance(String address, long assetId, int height) throws DataException { + String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? AND height <= ? ORDER BY height DESC LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId, height)) { + if (resultSet == null) + return null; + + BigDecimal balance = resultSet.getBigDecimal(1).setScale(8); + + return new AccountBalanceData(address, assetId, balance); + } catch (SQLException e) { + throw new DataException("Unable to fetch account balance from repository", e); + } + } + + @Override + public List getAssetBalances(long assetId, Boolean excludeZero) throws DataException { + StringBuilder sql = new StringBuilder(1024); + sql.append("SELECT account, IFNULL(balance, 0) FROM NewestAccountBalances WHERE asset_id = ?"); + + if (excludeZero != null && excludeZero) + sql.append(" AND balance != 0"); + + List accountBalances = new ArrayList<>(); + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), assetId)) { + if (resultSet == null) + return accountBalances; + + do { + String address = resultSet.getString(1); + BigDecimal balance = resultSet.getBigDecimal(2).setScale(8); + + accountBalances.add(new AccountBalanceData(address, assetId, balance)); + } while (resultSet.next()); + + return accountBalances; + } catch (SQLException e) { + throw new DataException("Unable to fetch asset balances from repository", e); + } + } + @Override public List getAssetBalances(List addresses, List assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException { @@ -291,10 +333,10 @@ public class HSQLDBAccountRepository implements AccountRepository { } sql.append(") AS Accounts (account) "); - sql.append("CROSS JOIN Assets LEFT OUTER JOIN AccountBalances USING (asset_id, account) "); + sql.append("CROSS JOIN Assets LEFT OUTER JOIN NewestAccountBalances USING (asset_id, account) "); } else { // Simplier, no-address query - sql.append("AccountBalances NATURAL JOIN Assets "); + sql.append("NewestAccountBalances NATURAL JOIN Assets "); } if (!assetIds.isEmpty()) { @@ -378,6 +420,10 @@ public class HSQLDBAccountRepository implements AccountRepository { accountBalanceData.getBalance()); try { + // Fill in 'height' + int height = this.repository.checkedExecute("SELECT COUNT(*) + 1 FROM Blocks").getInt(1); + saveHelper.bind("height", height); + saveHelper.execute(this.repository); } catch (SQLException e) { throw new DataException("Unable to save account balance into repository", e); @@ -387,12 +433,21 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public void delete(String address, long assetId) throws DataException { try { - this.repository.delete("AccountBalances", "account = ? and asset_id = ?", address, assetId); + this.repository.delete("AccountBalances", "account = ? AND asset_id = ?", address, assetId); } catch (SQLException e) { throw new DataException("Unable to delete account balance from repository", e); } } + @Override + public int deleteBalancesFromHeight(int height) throws DataException { + try { + return this.repository.delete("AccountBalances", "height >= ?", height); + } catch (SQLException e) { + throw new DataException("Unable to delete old account balances from repository", e); + } + } + // Reward-Share @Override @@ -695,4 +750,12 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + public int deleteQortFromQoraInfo(String address) throws DataException { + try { + return this.repository.delete("AccountQortFromQoraInfo", "account = ?", address); + } catch (SQLException e) { + throw new DataException("Unable to delete qort-from-qora info from 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 aa82a22f..4b88c19c 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -847,6 +847,23 @@ public class HSQLDBDatabaseUpdates { + "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); break; + case 60: // Adding height to account balances + // We need to drop primary key first + stmt.execute("ALTER TABLE AccountBalances DROP PRIMARY KEY"); + // Add height to account balances + stmt.execute("ALTER TABLE AccountBalances ADD COLUMN height INT NOT NULL DEFAULT 0 BEFORE BALANCE"); + // Add new primary key + stmt.execute("ALTER TABLE AccountBalances ADD PRIMARY KEY (asset_id, account, height)"); + /// Create a view for account balances at greatest height + stmt.execute("CREATE VIEW NewestAccountBalances (account, asset_id, balance) AS " + + "SELECT AccountBalances.account, AccountBalances.asset_id, AccountBalances.balance FROM AccountBalances " + + "LEFT OUTER JOIN AccountBalances AS NewerAccountBalances " + + "ON NewerAccountBalances.account = AccountBalances.account " + + "AND NewerAccountBalances.asset_id = AccountBalances.asset_id " + + "AND NewerAccountBalances.height > AccountBalances.height " + + "WHERE NewerAccountBalances.height IS NULL"); + break; + default: // nothing to do return false; diff --git a/src/test/java/org/qora/test/AccountBalanceTests.java b/src/test/java/org/qora/test/AccountBalanceTests.java new file mode 100644 index 00000000..899de3d0 --- /dev/null +++ b/src/test/java/org/qora/test/AccountBalanceTests.java @@ -0,0 +1,180 @@ +package org.qora.test; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.util.Random; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qora.account.Account; +import org.qora.account.PrivateKeyAccount; +import org.qora.account.PublicKeyAccount; +import org.qora.asset.Asset; +import org.qora.block.BlockChain; +import org.qora.data.account.AccountBalanceData; +import org.qora.data.transaction.BaseTransactionData; +import org.qora.data.transaction.PaymentTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.repository.DataException; +import org.qora.repository.Repository; +import org.qora.repository.RepositoryManager; +import org.qora.test.common.BlockUtils; +import org.qora.test.common.Common; +import org.qora.test.common.TestAccount; +import org.qora.test.common.TransactionUtils; + +public class AccountBalanceTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @After + public void afterTest() throws DataException { + Common.orphanCheck(); + } + + /** Tests that newer balances are returned instead of older ones. */ + @Test + public void testNewerBalance() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + TestAccount alice = Common.getTestAccount(repository, "alice"); + + testNewerBalance(repository, alice); + } + } + + private BigDecimal testNewerBalance(Repository repository, TestAccount testAccount) throws DataException { + // Grab initial balance + BigDecimal initialBalance = testAccount.getConfirmedBalance(Asset.QORT); + + // Mint block to cause newer balance + BlockUtils.mintBlock(repository); + + // Grab newer balance + BigDecimal newerBalance = testAccount.getConfirmedBalance(Asset.QORT); + + // Confirm newer balance is greater than initial balance + assertTrue("Newer balance should be greater than initial balance", newerBalance.compareTo(initialBalance) > 0); + + return initialBalance; + } + + /** Tests that orphaning reverts balance back to initial. */ + @Test + public void testOrphanedBalance() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + TestAccount alice = Common.getTestAccount(repository, "alice"); + + BigDecimal initialBalance = testNewerBalance(repository, alice); + + BlockUtils.orphanLastBlock(repository); + + // Grab post-orphan balance + BigDecimal orphanedBalance = alice.getConfirmedBalance(Asset.QORT); + + // Confirm post-orphan balance is same as initial + assertTrue("Post-orphan balance should match initial", orphanedBalance.equals(initialBalance)); + } + } + + /** Tests we can fetch initial balance when newer balance exists. */ + @Test + public void testGetBalanceAtHeight() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + TestAccount alice = Common.getTestAccount(repository, "alice"); + + BigDecimal initialBalance = testNewerBalance(repository, alice); + + // Fetch balance at height 1, even though newer balance exists + AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(alice.getAddress(), Asset.QORT, 1); + BigDecimal genesisBalance = accountBalanceData.getBalance(); + + // Confirm genesis balance is same as initial + assertTrue("Genesis balance should match initial", genesisBalance.equals(initialBalance)); + } + } + + /** Tests we can fetch balance with a height where no balance change occurred. */ + @Test + public void testGetBalanceAtNearestHeight() throws DataException { + Random random = new Random(); + + byte[] publicKey = new byte[32]; + random.nextBytes(publicKey); + + try (final Repository repository = RepositoryManager.getRepository()) { + PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, publicKey); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + + // Confirm recipient balance is zero + BigDecimal balance = recipientAccount.getConfirmedBalance(Asset.QORT); + assertTrue("recipient's balance should be zero", balance.signum() == 0); + + // Send 1 QORT to recipient + TestAccount sendingAccount = Common.getTestAccount(repository, "alice"); + pay(repository, sendingAccount, recipientAccount, BigDecimal.ONE); + + // Mint some more blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + + // Send more QORT to recipient + BigDecimal amount = BigDecimal.valueOf(random.nextInt(123456)); + pay(repository, sendingAccount, recipientAccount, amount); + + // Mint some more blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + + // Confirm recipient balance is as expected + BigDecimal totalAmount = amount.add(BigDecimal.ONE); + balance = recipientAccount.getConfirmedBalance(Asset.QORT); + assertTrue("recipient's balance incorrect", balance.compareTo(totalAmount) == 0); + + // Confirm balance as of 2 blocks ago + int height = repository.getBlockRepository().getBlockchainHeight(); + balance = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2).getBalance(); + assertTrue("recipient's historic balance incorrect", balance.compareTo(totalAmount) == 0); + + // Confirm balance prior to last payment + balance = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 15).getBalance(); + assertTrue("recipient's historic balance incorrect", balance.compareTo(BigDecimal.ONE) == 0); + + // Orphan blocks to before last payment + BlockUtils.orphanBlocks(repository, 10 + 5); + + // Re-check balance from (now) invalid height + AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2); + balance = accountBalanceData.getBalance(); + assertTrue("recipient's invalid-height balance should be one", balance.compareTo(BigDecimal.ONE) == 0); + + // Orphan blocks to before initial 1 QORT payment + BlockUtils.orphanBlocks(repository, 10 + 5); + + // Re-check balance from (now) invalid height + accountBalanceData = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2); + assertNull("recipient's invalid-height balance data should be null", accountBalanceData); + } + } + + private void pay(Repository repository, PrivateKeyAccount sendingAccount, Account recipientAccount, BigDecimal amount) throws DataException { + byte[] reference = sendingAccount.getLastReference(); + long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1; + + int txGroupId = 0; + BigDecimal fee = BlockChain.getInstance().getUnitFee(); + BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, sendingAccount.getPublicKey(), fee, null); + TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAccount.getAddress(), amount); + + TransactionUtils.signAndMint(repository, transactionData, sendingAccount); + } + +} \ No newline at end of file diff --git a/src/test/java/org/qora/test/minting/RewardTests.java b/src/test/java/org/qora/test/minting/RewardTests.java index 8104f8ed..9edb6720 100644 --- a/src/test/java/org/qora/test/minting/RewardTests.java +++ b/src/test/java/org/qora/test/minting/RewardTests.java @@ -1,5 +1,7 @@ package org.qora.test.minting; +import static org.junit.Assert.*; + import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; @@ -12,6 +14,7 @@ import org.qora.account.PrivateKeyAccount; import org.qora.asset.Asset; import org.qora.block.BlockChain; import org.qora.block.BlockChain.RewardByHeight; +import org.qora.data.account.AccountBalanceData; import org.qora.block.BlockMinter; import org.qora.repository.DataException; import org.qora.repository.Repository; @@ -105,14 +108,47 @@ public class RewardTests extends Common { BigDecimal qoraPerQort = BlockChain.getInstance().getQoraPerQortReward(); try (final Repository repository = RepositoryManager.getRepository()) { - Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.QORT_FROM_QORA); + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); BigDecimal blockReward = BlockUtils.getNextBlockReward(repository); + // Fetch all legacy QORA holder balances + List qoraHolders = repository.getAccountRepository().getAssetBalances(Asset.LEGACY_QORA, true); + BigDecimal totalQoraHeld = BigDecimal.ZERO.setScale(8); + for (AccountBalanceData accountBalanceData : qoraHolders) + totalQoraHeld = totalQoraHeld.add(accountBalanceData.getBalance()); + BlockUtils.mintBlock(repository); + /* + * Example: + * + * Block reward is 100 QORT, QORA-holders' share is 0.20 (20%) = 20 QORT + * + * We hold 100 QORA + * Someone else holds 28 QORA + * Total QORA held: 128 QORA + * + * Our portion of that is 100 QORA / 128 QORA * 20 QORT = 15.625 QORT + * + * QORA holders earn at most 1 QORT per 250 QORA held. + * + * So we can earn at most 100 QORA / 250 QORAperQORT = 0.4 QORT + * + * Thus our block earning should be capped to 0.4 QORT. + */ + // Expected reward - BigDecimal expectedReward = blockReward.multiply(qoraHoldersShare).divide(qoraPerQort, RoundingMode.DOWN); + BigDecimal qoraHoldersReward = blockReward.multiply(qoraHoldersShare); + assertTrue("QORA-holders share of block reward should be less than total block reward", qoraHoldersReward.compareTo(blockReward) < 0); + + BigDecimal ourQoraHeld = initialBalances.get("chloe").get(Asset.LEGACY_QORA); + BigDecimal ourQoraReward = qoraHoldersReward.multiply(ourQoraHeld).divide(totalQoraHeld, RoundingMode.DOWN).setScale(8, RoundingMode.DOWN); + assertTrue("Our QORA-related reward should be less than total QORA-holders share of block reward", ourQoraReward.compareTo(qoraHoldersReward) < 0); + + BigDecimal ourQortFromQoraCap = ourQoraHeld.divide(qoraPerQort, RoundingMode.DOWN); + + BigDecimal expectedReward = ourQoraReward.min(ourQortFromQoraCap); 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)); diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index fd230218..615df136 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -53,7 +53,8 @@ { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" }, { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" }, - { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "100", "assetId": 1 }, + { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "10000", "assetId": 1 }, + { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "8", "assetId": 1 }, { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },