From 90e8cfc737bf38f8104277a3aa0e1323dfcfdf64 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 8 Jul 2022 11:12:58 +0100 Subject: [PATCH 1/5] qoraHoldersShare reworked to qoraHoldersShareByHeight. This allows the QORA share percentage to be modified at different heights, based on community votes. Added unit test to simulate a reduction. --- src/main/java/org/qortal/block/Block.java | 2 +- .../java/org/qortal/block/BlockChain.java | 30 ++++++++---- src/main/resources/blockchain.json | 5 +- .../org/qortal/test/minting/RewardTests.java | 47 ++++++++++++++++++- .../test-chain-v2-block-timestamps.json | 5 +- .../test-chain-v2-disable-reference.json | 5 +- .../test-chain-v2-founder-rewards.json | 5 +- .../test-chain-v2-leftover-reward.json | 5 +- src/test/resources/test-chain-v2-minting.json | 5 +- .../test-chain-v2-qora-holder-extremes.json | 5 +- .../resources/test-chain-v2-qora-holder.json | 5 +- .../test-chain-v2-reward-levels.json | 5 +- .../test-chain-v2-reward-scaling.json | 5 +- .../test-chain-v2-reward-shares.json | 5 +- src/test/resources/test-chain-v2.json | 5 +- 15 files changed, 114 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index ddfe247a..bbc6e31b 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1914,7 +1914,7 @@ public class Block { // Fetch list of legacy QORA holders who haven't reached their cap of QORT reward. List qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight()); final boolean haveQoraHolders = !qoraHolders.isEmpty(); - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight()); // Perform account-level-based reward scaling if appropriate if (!haveFounders) { diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 1dbc9a23..239ebaa2 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -113,9 +113,13 @@ public class BlockChain { /** Generated lookup of share-bin by account level */ private AccountLevelShareBin[] shareBinsByLevel; - /** Share of block reward/fees to legacy QORA coin holders */ - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private Long qoraHoldersShare; + /** Share of block reward/fees to legacy QORA coin holders, by block height */ + public static class ShareByHeight { + public int height; + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long share; + } + private List qoraHoldersShareByHeight; /** How many legacy QORA per 1 QORT of block reward. */ @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @@ -354,10 +358,6 @@ public class BlockChain { return this.cumulativeBlocksByLevel; } - public long getQoraHoldersShare() { - return this.qoraHoldersShare; - } - public long getQoraPerQortReward() { return this.qoraPerQortReward; } @@ -468,6 +468,15 @@ public class BlockChain { return 0; } + public long getQoraHoldersShareAtHeight(int ourHeight) { + // Scan through for QORA share at our height + for (int i = qoraHoldersShareByHeight.size() - 1; i >= 0; --i) + if (qoraHoldersShareByHeight.get(i).height <= ourHeight) + return qoraHoldersShareByHeight.get(i).share; + + return 0; + } + /** Validate blockchain config read from JSON */ private void validateConfig() { if (this.genesisInfo == null) @@ -479,8 +488,8 @@ public class BlockChain { if (this.sharesByLevel == null) Settings.throwValidationError("No \"sharesByLevel\" entry found in blockchain config"); - if (this.qoraHoldersShare == null) - Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config"); + if (this.qoraHoldersShareByHeight == null) + Settings.throwValidationError("No \"qoraHoldersShareByHeight\" entry found in blockchain config"); if (this.qoraPerQortReward == null) Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config"); @@ -518,7 +527,7 @@ public class BlockChain { Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name())); // Check block reward share bounds - long totalShare = this.qoraHoldersShare; + long totalShare = this.getQoraHoldersShareAtHeight(1); // Add share percents for account-level-based rewards for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel) totalShare += accountLevelShareBin.share; @@ -556,6 +565,7 @@ public class BlockChain { this.blocksNeededByLevel = Collections.unmodifiableList(this.blocksNeededByLevel); this.cumulativeBlocksByLevel = Collections.unmodifiableList(this.cumulativeBlocksByLevel); this.blockTimingsByHeight = Collections.unmodifiableList(this.blockTimingsByHeight); + this.qoraHoldersShareByHeight = Collections.unmodifiableList(this.qoraHoldersShareByHeight); } /** diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index b65fd72e..9f9d3a2b 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -45,7 +45,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 9999999, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ], "blockTimingsByHeight": [ diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index f7970ace..658f285f 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -4,6 +4,7 @@ import static org.junit.Assert.*; import java.math.BigInteger; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -14,6 +15,7 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.block.BlockChain.RewardByHeight; import org.qortal.controller.BlockMinter; @@ -109,7 +111,7 @@ public class RewardTests extends Common { public void testLegacyQoraReward() throws DataException { Common.useSettings("test-settings-v2-qora-holder-extremes.json"); - long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); BigInteger qoraHoldersShareBI = BigInteger.valueOf(qoraHoldersShare); long qoraPerQort = BlockChain.getInstance().getQoraPerQortReward(); @@ -190,6 +192,47 @@ public class RewardTests extends Common { } } + @Test + public void testLegacyQoraRewardReduction() throws DataException { + Common.useSettings("test-settings-v2-qora-holder-extremes.json"); + + // Make sure that the QORA share reduces between blocks 4 and 5 + assertTrue(BlockChain.getInstance().getQoraHoldersShareAtHeight(5) < BlockChain.getInstance().getQoraHoldersShareAtHeight(4)); + + // Keep track of balance deltas at each height + Map chloeQortBalanceDeltaAtEachHeight = new HashMap<>(); + + try (final Repository repository = RepositoryManager.getRepository()) { + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + long chloeLastQortBalance = initialBalances.get("chloe").get(Asset.QORT); + + for (int i=2; i<=10; i++) { + + Block block = BlockUtils.mintBlock(repository); + + // Add to map of balance deltas at each height + long chloeNewQortBalance = AccountUtils.getBalance(repository, "chloe", Asset.QORT); + chloeQortBalanceDeltaAtEachHeight.put(block.getBlockData().getHeight(), chloeNewQortBalance - chloeLastQortBalance); + chloeLastQortBalance = chloeNewQortBalance; + } + + // Ensure blocks 2-4 paid out the same rewards to Chloe + assertEquals(chloeQortBalanceDeltaAtEachHeight.get(2), chloeQortBalanceDeltaAtEachHeight.get(4)); + + // Ensure block 5 paid a lower reward + assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) < chloeQortBalanceDeltaAtEachHeight.get(4)); + + // Check that the reward was 20x lower + assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) == chloeQortBalanceDeltaAtEachHeight.get(4) / 20); + + // Orphan to block 4 and ensure that Chloe's balance hasn't been incorrectly affected by the reward reduction + BlockUtils.orphanToBlock(repository, 4); + long expectedChloeQortBalance = initialBalances.get("chloe").get(Asset.QORT) + chloeQortBalanceDeltaAtEachHeight.get(2) + + chloeQortBalanceDeltaAtEachHeight.get(3) + chloeQortBalanceDeltaAtEachHeight.get(4); + assertEquals(expectedChloeQortBalance, AccountUtils.getBalance(repository, "chloe", Asset.QORT)); + } + } + /** Use Alice-Chloe reward-share to bump Chloe from level 0 to level 1, then check orphaning works as expected. */ @Test public void testLevel1() throws DataException { @@ -295,7 +338,7 @@ public class RewardTests extends Common { * So Dilbert should receive 100% - legacy QORA holder's share. */ - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); final long remainingShare = 1_00000000 - qoraHoldersShare; long dilbertExpectedBalance = initialBalances.get("dilbert").get(Asset.QORT); diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 38a18a8c..782f6152 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -26,7 +26,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 648e91b5..633d8aa4 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 540d7efd..f4d39517 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index ffd81379..e2578260 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 8d66e072..d1ea3992 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 9e8ff2a8..da6f25d9 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 5, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 0dac2457..9d4f1777 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 90d201a3..949ae5c0 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 6b2cbc0c..2a7d830b 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 9e713095..4b800c83 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index c08dac04..832be222 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ From 35f343068710ff28a5f2b99722c6d15046e8f896 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 10 Jul 2022 12:09:44 +0100 Subject: [PATCH 2/5] Added share bin activation feature. To prevent a single or very small number of minters receiving the rewards for an entire tier, share bins can now require "activation". This adds the requirement that a minimum number of accounts must be present in a share bin before it is considered active. When inactive, the rewards and minters are added to the previous tier. Summary of new functionality: - If a share bin has more than one, but less than 30 accounts present, the rewards and accounts are shifted to the previous share bin. - This process is iterative, so the accounts can shift through multiple tiers until the minimum number of accounts is met, OR the share bin's starting level is less than shareBinActivationMinLevel. - Applies to level 7+, so that no backwards support is needed. It will only take effect once the first account reaches level 7. This requires hot swapping the sharesByLevel data to combine tiers where needed, so is a considerable shift away from the immutable array that was in place previously. All existing and new unit tests are now passing, however a lot more testing will be needed. --- src/main/java/org/qortal/block/Block.java | 70 ++++++- .../java/org/qortal/block/BlockChain.java | 29 ++- src/main/resources/blockchain.json | 12 +- .../org/qortal/test/minting/RewardTests.java | 175 +++++++++++++++++- .../test-chain-v2-block-timestamps.json | 12 +- .../test-chain-v2-disable-reference.json | 12 +- .../test-chain-v2-founder-rewards.json | 12 +- .../test-chain-v2-leftover-reward.json | 12 +- src/test/resources/test-chain-v2-minting.json | 12 +- .../test-chain-v2-qora-holder-extremes.json | 12 +- .../resources/test-chain-v2-qora-holder.json | 12 +- .../test-chain-v2-reward-levels.json | 12 +- .../test-chain-v2-reward-scaling.json | 12 +- .../test-chain-v2-reward-shares.json | 12 +- src/test/resources/test-chain-v2.json | 12 +- 15 files changed, 348 insertions(+), 70 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index bbc6e31b..8fc2b0b9 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -199,6 +199,11 @@ public class Block { } + public boolean hasShareBin(AccountLevelShareBin shareBin, int blockHeight) { + AccountLevelShareBin ourShareBin = this.getShareBin(blockHeight); + return ourShareBin != null && shareBin.id == ourShareBin.id; + } + public long distribute(long accountAmount, Map balanceChanges) { if (this.isRecipientAlsoMinter) { // minter & recipient the same - simpler case @@ -1891,12 +1896,67 @@ public class Block { final boolean haveFounders = !onlineFounderAccounts.isEmpty(); // Determine reward candidates based on account level - List accountLevelShareBins = BlockChain.getInstance().getAccountLevelShareBins(); - for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) { - // Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out. + // This needs a deep copy, so the shares can be modified when tiers aren't activated yet + List accountLevelShareBins = new ArrayList<>(); + for (AccountLevelShareBin accountLevelShareBin : BlockChain.getInstance().getAccountLevelShareBins()) { + accountLevelShareBins.add((AccountLevelShareBin) accountLevelShareBin.clone()); + } + + Map> accountsForShareBin = new HashMap<>(); + + // We might need to combine some share bins if they haven't reached the minimum number of minters yet + for (int binIndex = accountLevelShareBins.size()-1; binIndex >= 0; --binIndex) { AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex); - // Object reference compare is OK as all references are read-only from blockchain config. - List binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin(this.blockData.getHeight()) == accountLevelShareBin).collect(Collectors.toList()); + + // Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out. + List binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.hasShareBin(accountLevelShareBin, this.blockData.getHeight())).collect(Collectors.toList()); + // Add any accounts that have been moved down from a higher tier + List existingBinnedAccounts = accountsForShareBin.get(binIndex); + if (existingBinnedAccounts != null) + binnedAccounts.addAll(existingBinnedAccounts); + + // Logic below may only apply to higher levels, and only for share bins with a specific range of online accounts + if (accountLevelShareBin.levels.get(0) < BlockChain.getInstance().getShareBinActivationMinLevel() || + binnedAccounts.isEmpty() || binnedAccounts.size() >= BlockChain.getInstance().getMinAccountsToActivateShareBin()) { + // Add all accounts for this share bin to the accountsForShareBin list + accountsForShareBin.put(binIndex, binnedAccounts); + continue; + } + + // Share bin contains more than one, but less than the minimum number of minters. We treat this share bin + // as not activated yet. In these cases, the rewards and minters are combined and paid out to the previous + // share bin, to prevent a single or handful of accounts receiving the entire rewards for a share bin. + // + // Example: + // + // - Share bin for levels 5 and 6 has 100 minters + // - Share bin for levels 7 and 8 has 10 minters + // + // This is below the minimum of 30, so share bins are reconstructed as follows: + // + // - Share bin for levels 5 and 6 now contains 110 minters + // - Share bin for levels 7 and 8 now contains 0 minters + // - Share bin for levels 5 and 6 now pays out rewards for levels 5, 6, 7, and 8 + // - Share bin for levels 7 and 8 pays zero rewards + // + // This process is iterative, so will combine several tiers if needed. + + // Designate this share bin as empty + accountsForShareBin.put(binIndex, new ArrayList<>()); + + // Move the accounts originally intended for this share bin to the previous one + accountsForShareBin.put(binIndex - 1, binnedAccounts); + + // Move the block reward from this share bin to the previous one + AccountLevelShareBin previousShareBin = accountLevelShareBins.get(binIndex - 1); + previousShareBin.share += accountLevelShareBin.share; + accountLevelShareBin.share = 0L; + } + + // Now loop through (potentially modified) share bins and determine the reward candidates + for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) { + AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex); + List binnedAccounts = accountsForShareBin.get(binIndex); // No online accounts in this bin? Skip to next one if (binnedAccounts.isEmpty()) diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 239ebaa2..412f8ede 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -104,10 +104,23 @@ public class BlockChain { private List rewardsByHeight; /** Share of block reward/fees by account level */ - public static class AccountLevelShareBin { + public static class AccountLevelShareBin implements Cloneable { + public int id; public List levels; @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) public long share; + + public Object clone() { + AccountLevelShareBin shareBinCopy = new AccountLevelShareBin(); + List levelsCopy = new ArrayList<>(); + for (Integer level : this.levels) { + levelsCopy.add(level); + } + shareBinCopy.id = this.id; + shareBinCopy.levels = levelsCopy; + shareBinCopy.share = this.share; + return shareBinCopy; + } } private List sharesByLevel; /** Generated lookup of share-bin by account level */ @@ -125,6 +138,12 @@ public class BlockChain { @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) private Long qoraPerQortReward; + /** Minimum number of accounts before a share bin is considered activated */ + private int minAccountsToActivateShareBin; + + /** Min level at which share bin activation takes place; lower levels allow less than minAccountsPerShareBin */ + private int shareBinActivationMinLevel; + /** * Number of minted blocks required to reach next level from previous. *

@@ -362,6 +381,14 @@ public class BlockChain { return this.qoraPerQortReward; } + public int getMinAccountsToActivateShareBin() { + return this.minAccountsToActivateShareBin; + } + + public int getShareBinActivationMinLevel() { + return this.shareBinActivationMinLevel; + } + public int getMinAccountLevelToMint() { return this.minAccountLevelToMint; } diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 9f9d3a2b..73e7b98e 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -39,17 +39,19 @@ { "height": 3110401, "reward": 2.00 } ], "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 9999999, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index 658f285f..817d1bb8 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -3,11 +3,9 @@ package org.qortal.test.minting; import static org.junit.Assert.*; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.After; @@ -833,6 +831,175 @@ public class RewardTests extends Common { } } + /** Test rewards for level 7 and 8 accounts, when the tier doesn't yet have enough minters in it */ + @Test + public void testLevel7And8RewardsPreActivation() throws DataException, IllegalAccessException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + // Set minAccountsToActivateShareBin to 3 so that share bins 7-8 and 9-10 are considered inactive + FieldUtils.writeField(BlockChain.getInstance(), "minAccountsToActivateShareBin", 3, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share NOT online + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint enough blocks to bump testAccount levels to 7 and 8 + final int minterBlocksNeeded = cumulativeBlocksByLevel.get(8) - 20; // 20 blocks before level 8, so that the test accounts reach the correct levels + for (int bc = 0; bc < minterBlocksNeeded; ++bc) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect + assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(7, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(8, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are using the correct block reward value + assertEquals(100000000L, blockReward); + + /* + * Alice, Chloe, and Dilbert are 'online'. + * Chloe is level 7; Dilbert is level 8. + * One founder online (Alice, who is also level 7). + * No legacy QORA holders. + * + * Level 7 and 8 is not yet activated, so its rewards are added to the level 5 and 6 share bin. + * There are no level 5 and 6 online. + * Chloe and Dilbert should receive equal shares of the 35% block reward for levels 5 to 8. + * Alice should receive the remainder (65%). + */ + + final int level5To8SharePercent = 35_00; // 35% (combined 15% and 20%) + final long level5To8ShareAmount = (blockReward * level5To8SharePercent) / 100L / 100L; + final long expectedLevel5To8Reward = level5To8ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderReward = blockReward - level5To8ShareAmount; // Alice should receive the remainder + + // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To8Reward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To8Reward); + + } + } + + /** Test rewards for level 9 and 10 accounts, when the tier doesn't yet have enough minters in it. + * Tier 7-8 isn't activated either, so the rewards and minters are all moved to tier 5-6. */ + @Test + public void testLevel9And10RewardsPreActivation() throws DataException, IllegalAccessException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + // Set minAccountsToActivateShareBin to 3 so that share bins 7-8 and 9-10 are considered inactive + FieldUtils.writeField(BlockChain.getInstance(), "minAccountsToActivateShareBin", 3, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share not initially online + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint enough blocks to bump testAccount levels to 9 and 10 + final int minterBlocksNeeded = cumulativeBlocksByLevel.get(10) - 20; // 20 blocks before level 10, so that the test accounts reach the correct levels + for (int bc = 0; bc < minterBlocksNeeded; ++bc) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Bob self-share now comes online + byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0); + PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey); + mintingAndOnlineAccounts.add(bobRewardShareAccount); + + // Ensure that the levels are as we expect + assertEquals(9, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(9, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(10, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are using the correct block reward value + assertEquals(100000000L, blockReward); + + /* + * Alice, Bob, Chloe, and Dilbert are 'online'. + * Bob is level 1; Chloe is level 9; Dilbert is level 10. + * One founder online (Alice, who is also level 9). + * No legacy QORA holders. + * + * Levels 7+8, and 9+10 are not yet activated, so their rewards are added to the level 5 and 6 share bin. + * There are no levels 5-8 online. + * Chloe and Dilbert should receive equal shares of the 60% block reward for levels 5 to 10. + * Alice should receive the remainder (40%). + */ + + final int level1And2SharePercent = 5_00; // 5% + final int level5To10SharePercent = 60_00; // 60% (combined 15%, 20%, and 25%) + final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L; + final long level5To10ShareAmount = (blockReward * level5To10SharePercent) / 100L / 100L; + final long expectedLevel1And2Reward = level1And2ShareAmount; // The reward is given entirely to Bob + final long expectedLevel5To10Reward = level5To10ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderReward = blockReward - level1And2ShareAmount - level5To10ShareAmount; // Alice should receive the remainder + + // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedLevel1And2Reward); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To10Reward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To10Reward); + + } + } + private int getFlags(Repository repository, String name) throws DataException { TestAccount testAccount = Common.getTestAccount(repository, name); diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 782f6152..d041463f 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -20,17 +20,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 633d8aa4..8a4a58cc 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index f4d39517..9f588e7b 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index e2578260..c49e79f5 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index d1ea3992..3a214599 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index da6f25d9..19fbdafd 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 5, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 9d4f1777..996fbbe0 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 949ae5c0..5f75b844 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 1, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 2a7d830b..9107074c 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 4b800c83..74ce003c 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 832be222..824ba272 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -24,17 +24,19 @@ { "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 } + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], "qoraHoldersShareByHeight": [ { "height": 1, "share": 0.20 }, { "height": 1000000, "share": 0.01 } ], "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } From 3d2144f3030455a079f546c350bc85a168c9bc1b Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 16 Jul 2022 17:29:16 +0100 Subject: [PATCH 3/5] Check orphaning in levels 7-8 and 9-10 reward tests. This would have been tested in orphanCheck() anyway, but this makes the testing a bit more granular. --- .../org/qortal/test/minting/RewardTests.java | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index 817d1bb8..0745d6da 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -743,6 +743,15 @@ public class RewardTests extends Common { AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel7And8Reward); AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel7And8Reward); + // Orphan and ensure balances return to their previous values + BlockUtils.orphanBlocks(repository, 1); + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + } } @@ -828,6 +837,15 @@ public class RewardTests extends Common { AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel9And10Reward); AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel9And10Reward); + // Orphan and ensure balances return to their previous values + BlockUtils.orphanBlocks(repository, 1); + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + } } @@ -902,12 +920,21 @@ public class RewardTests extends Common { final long expectedLevel5To8Reward = level5To8ShareAmount / 2; // The reward is split between Chloe and Dilbert final long expectedFounderReward = blockReward - level5To8ShareAmount; // Alice should receive the remainder - // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + // Validate the balances AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To8Reward); AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To8Reward); + // Orphan and ensure balances return to their previous values + BlockUtils.orphanBlocks(repository, 1); + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + } } @@ -991,12 +1018,21 @@ public class RewardTests extends Common { final long expectedLevel5To10Reward = level5To10ShareAmount / 2; // The reward is split between Chloe and Dilbert final long expectedFounderReward = blockReward - level1And2ShareAmount - level5To10ShareAmount; // Alice should receive the remainder - // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + // Validate the balances AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedLevel1And2Reward); AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To10Reward); AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To10Reward); + // Orphan and ensure balances return to their previous values + BlockUtils.orphanBlocks(repository, 1); + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + } } From 508a34684b83da9a37073484263cf91892d76e3d Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 16 Jul 2022 18:45:49 +0100 Subject: [PATCH 4/5] Revert "qoraHoldersShare reworked to qoraHoldersShareByHeight." This reverts commit 90e8cfc737bf38f8104277a3aa0e1323dfcfdf64. # Conflicts: # src/test/java/org/qortal/test/minting/RewardTests.java --- src/main/java/org/qortal/block/Block.java | 2 +- .../java/org/qortal/block/BlockChain.java | 30 ++++-------- src/main/resources/blockchain.json | 5 +- .../org/qortal/test/minting/RewardTests.java | 46 +------------------ .../test-chain-v2-block-timestamps.json | 5 +- .../test-chain-v2-disable-reference.json | 5 +- .../test-chain-v2-founder-rewards.json | 5 +- .../test-chain-v2-leftover-reward.json | 5 +- src/test/resources/test-chain-v2-minting.json | 5 +- .../test-chain-v2-qora-holder-extremes.json | 5 +- .../resources/test-chain-v2-qora-holder.json | 5 +- .../test-chain-v2-reward-levels.json | 5 +- .../test-chain-v2-reward-scaling.json | 5 +- .../test-chain-v2-reward-shares.json | 5 +- src/test/resources/test-chain-v2.json | 5 +- 15 files changed, 25 insertions(+), 113 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 8fc2b0b9..c48ab234 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1974,7 +1974,7 @@ public class Block { // Fetch list of legacy QORA holders who haven't reached their cap of QORT reward. List qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight()); final boolean haveQoraHolders = !qoraHolders.isEmpty(); - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight()); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); // Perform account-level-based reward scaling if appropriate if (!haveFounders) { diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 412f8ede..96f94b6c 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -126,13 +126,9 @@ public class BlockChain { /** Generated lookup of share-bin by account level */ private AccountLevelShareBin[] shareBinsByLevel; - /** Share of block reward/fees to legacy QORA coin holders, by block height */ - public static class ShareByHeight { - public int height; - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long share; - } - private List qoraHoldersShareByHeight; + /** Share of block reward/fees to legacy QORA coin holders */ + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + private Long qoraHoldersShare; /** How many legacy QORA per 1 QORT of block reward. */ @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @@ -377,6 +373,10 @@ public class BlockChain { return this.cumulativeBlocksByLevel; } + public long getQoraHoldersShare() { + return this.qoraHoldersShare; + } + public long getQoraPerQortReward() { return this.qoraPerQortReward; } @@ -495,15 +495,6 @@ public class BlockChain { return 0; } - public long getQoraHoldersShareAtHeight(int ourHeight) { - // Scan through for QORA share at our height - for (int i = qoraHoldersShareByHeight.size() - 1; i >= 0; --i) - if (qoraHoldersShareByHeight.get(i).height <= ourHeight) - return qoraHoldersShareByHeight.get(i).share; - - return 0; - } - /** Validate blockchain config read from JSON */ private void validateConfig() { if (this.genesisInfo == null) @@ -515,8 +506,8 @@ public class BlockChain { if (this.sharesByLevel == null) Settings.throwValidationError("No \"sharesByLevel\" entry found in blockchain config"); - if (this.qoraHoldersShareByHeight == null) - Settings.throwValidationError("No \"qoraHoldersShareByHeight\" entry found in blockchain config"); + 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"); @@ -554,7 +545,7 @@ public class BlockChain { Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name())); // Check block reward share bounds - long totalShare = this.getQoraHoldersShareAtHeight(1); + long totalShare = this.qoraHoldersShare; // Add share percents for account-level-based rewards for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel) totalShare += accountLevelShareBin.share; @@ -592,7 +583,6 @@ public class BlockChain { this.blocksNeededByLevel = Collections.unmodifiableList(this.blocksNeededByLevel); this.cumulativeBlocksByLevel = Collections.unmodifiableList(this.cumulativeBlocksByLevel); this.blockTimingsByHeight = Collections.unmodifiableList(this.blockTimingsByHeight); - this.qoraHoldersShareByHeight = Collections.unmodifiableList(this.qoraHoldersShareByHeight); } /** diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 73e7b98e..94f9c666 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -45,10 +45,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 9999999, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index 0745d6da..229495c4 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -13,7 +13,6 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.asset.Asset; -import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.block.BlockChain.RewardByHeight; import org.qortal.controller.BlockMinter; @@ -109,7 +108,7 @@ public class RewardTests extends Common { public void testLegacyQoraReward() throws DataException { Common.useSettings("test-settings-v2-qora-holder-extremes.json"); - long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); + long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); BigInteger qoraHoldersShareBI = BigInteger.valueOf(qoraHoldersShare); long qoraPerQort = BlockChain.getInstance().getQoraPerQortReward(); @@ -190,47 +189,6 @@ public class RewardTests extends Common { } } - @Test - public void testLegacyQoraRewardReduction() throws DataException { - Common.useSettings("test-settings-v2-qora-holder-extremes.json"); - - // Make sure that the QORA share reduces between blocks 4 and 5 - assertTrue(BlockChain.getInstance().getQoraHoldersShareAtHeight(5) < BlockChain.getInstance().getQoraHoldersShareAtHeight(4)); - - // Keep track of balance deltas at each height - Map chloeQortBalanceDeltaAtEachHeight = new HashMap<>(); - - try (final Repository repository = RepositoryManager.getRepository()) { - Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); - long chloeLastQortBalance = initialBalances.get("chloe").get(Asset.QORT); - - for (int i=2; i<=10; i++) { - - Block block = BlockUtils.mintBlock(repository); - - // Add to map of balance deltas at each height - long chloeNewQortBalance = AccountUtils.getBalance(repository, "chloe", Asset.QORT); - chloeQortBalanceDeltaAtEachHeight.put(block.getBlockData().getHeight(), chloeNewQortBalance - chloeLastQortBalance); - chloeLastQortBalance = chloeNewQortBalance; - } - - // Ensure blocks 2-4 paid out the same rewards to Chloe - assertEquals(chloeQortBalanceDeltaAtEachHeight.get(2), chloeQortBalanceDeltaAtEachHeight.get(4)); - - // Ensure block 5 paid a lower reward - assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) < chloeQortBalanceDeltaAtEachHeight.get(4)); - - // Check that the reward was 20x lower - assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) == chloeQortBalanceDeltaAtEachHeight.get(4) / 20); - - // Orphan to block 4 and ensure that Chloe's balance hasn't been incorrectly affected by the reward reduction - BlockUtils.orphanToBlock(repository, 4); - long expectedChloeQortBalance = initialBalances.get("chloe").get(Asset.QORT) + chloeQortBalanceDeltaAtEachHeight.get(2) + - chloeQortBalanceDeltaAtEachHeight.get(3) + chloeQortBalanceDeltaAtEachHeight.get(4); - assertEquals(expectedChloeQortBalance, AccountUtils.getBalance(repository, "chloe", Asset.QORT)); - } - } - /** Use Alice-Chloe reward-share to bump Chloe from level 0 to level 1, then check orphaning works as expected. */ @Test public void testLevel1() throws DataException { @@ -336,7 +294,7 @@ public class RewardTests extends Common { * So Dilbert should receive 100% - legacy QORA holder's share. */ - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); final long remainingShare = 1_00000000 - qoraHoldersShare; long dilbertExpectedBalance = initialBalances.get("dilbert").get(Asset.QORT); diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index d041463f..d7beba3c 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -26,10 +26,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 8a4a58cc..3ad7da60 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 9f588e7b..ac11d0f1 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index c49e79f5..61917115 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 3a214599..5fed0b35 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 19fbdafd..ba1fbff5 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 5, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 996fbbe0..d0007e63 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 5f75b844..e8c7ca9a 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 1, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 9107074c..8c22c4ed 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 74ce003c..8f14e48f 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 824ba272..e5bd6dba 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -30,10 +30,7 @@ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShareByHeight": [ - { "height": 1, "share": 0.20 }, - { "height": 1000000, "share": 0.01 } - ], + "qoraHoldersShare": 0.20, "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, From 97221a44491da80c5d8e570d2fb895069ae9f922 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 17 Jul 2022 13:37:52 +0100 Subject: [PATCH 5/5] Added test to simulate level 7-8 reward tier activation, including orphaning. --- .../org/qortal/test/minting/RewardTests.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index 229495c4..4aee2de1 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -994,6 +994,152 @@ public class RewardTests extends Common { } } + /** Test rewards for level 7 and 8 accounts, when the tier reaches the minimum number of accounts */ + @Test + public void testLevel7And8RewardsPreAndPostActivation() throws DataException, IllegalAccessException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + // Set minAccountsToActivateShareBin to 2 so that share bins 7-8 and 9-10 are considered inactive at first + FieldUtils.writeField(BlockChain.getInstance(), "minAccountsToActivateShareBin", 2, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share NOT online + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint enough blocks to bump two of the testAccount levels to 7 + final int minterBlocksNeeded = cumulativeBlocksByLevel.get(7) - 12; // 12 blocks before level 7, so that dilbert and alice have reached level 7, but chloe will reach it in the next 2 blocks + for (int bc = 0; bc < minterBlocksNeeded; ++bc) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect + assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(6, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(7, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Now that dilbert has reached level 7, we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are using the correct block reward value + assertEquals(100000000L, blockReward); + + /* + * Alice, Chloe, and Dilbert are 'online'. + * Chloe is level 6; Dilbert is level 7. + * One founder online (Alice, who is also level 7). + * No legacy QORA holders. + * + * Level 7 and 8 is not yet activated, so its rewards are added to the level 5 and 6 share bin. + * There are no level 5 and 6 online. + * Chloe and Dilbert should receive equal shares of the 35% block reward for levels 5 to 8. + * Alice should receive the remainder (65%). + */ + + final int level5To8SharePercent = 35_00; // 35% (combined 15% and 20%) + final long level5To8ShareAmount = (blockReward * level5To8SharePercent) / 100L / 100L; + final long expectedLevel5To8Reward = level5To8ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderReward = blockReward - level5To8ShareAmount; // Alice should receive the remainder + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To8Reward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To8Reward); + + // Ensure that the levels are as we expect + assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(6, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(7, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Capture pre-activation balances + Map> preActivationBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long alicePreActivationBalance = preActivationBalances.get("alice").get(Asset.QORT); + final long bobPreActivationBalance = preActivationBalances.get("bob").get(Asset.QORT); + final long chloePreActivationBalance = preActivationBalances.get("chloe").get(Asset.QORT); + final long dilbertPreActivationBalance = preActivationBalances.get("dilbert").get(Asset.QORT); + + // Mint another block + blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect (chloe has now increased to level 7; level 7-8 is now activated) + assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(7, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(7, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + /* + * Alice, Chloe, and Dilbert are 'online'. + * Chloe and Dilbert are level 7. + * One founder online (Alice, who is also level 7). + * No legacy QORA holders. + * + * Level 7 and 8 is now activated, so its rewards are paid out in the normal way. + * There are no level 5 and 6 online. + * Chloe and Dilbert should receive equal shares of the 20% block reward for levels 7 to 8. + * Alice should receive the remainder (80%). + */ + + final int level7To8SharePercent = 20_00; // 20% + final long level7To8ShareAmount = (blockReward * level7To8SharePercent) / 100L / 100L; + final long expectedLevel7To8Reward = level7To8ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long newExpectedFounderReward = blockReward - level7To8ShareAmount; // Alice should receive the remainder + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, alicePreActivationBalance+newExpectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobPreActivationBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloePreActivationBalance+expectedLevel7To8Reward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertPreActivationBalance+expectedLevel7To8Reward); + + + // Orphan and ensure balances return to their pre-activation values + BlockUtils.orphanBlocks(repository, 1); + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, alicePreActivationBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobPreActivationBalance); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloePreActivationBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertPreActivationBalance); + + + // Orphan again and ensure balances return to their initial values + BlockUtils.orphanBlocks(repository, 1); + + // Validate the balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + + } + } + private int getFlags(Repository repository, String name) throws DataException { TestAccount testAccount = Common.getTestAccount(repository, name);