diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index ddfe247a..c48ab234 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 1dbc9a23..96f94b6c 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 */ @@ -121,6 +134,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 b65fd72e..94f9c666 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -39,14 +39,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 f7970ace..4aee2de1 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -3,10 +3,9 @@ package org.qortal.test.minting; import static org.junit.Assert.*; import java.math.BigInteger; -import java.util.ArrayList; -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; @@ -702,6 +701,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); + } } @@ -787,6 +795,348 @@ 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); + + } + } + + /** 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 + 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); + + } + } + + /** 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 + 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); + + } + } + + /** 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); + } } diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 38a18a8c..d7beba3c 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -20,14 +20,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 648e91b5..3ad7da60 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 540d7efd..ac11d0f1 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 ffd81379..61917115 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 8d66e072..5fed0b35 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 9e8ff2a8..ba1fbff5 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 0dac2457..d0007e63 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 90d201a3..e8c7ca9a 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 6b2cbc0c..8c22c4ed 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 9e713095..8f14e48f 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 c08dac04..e5bd6dba 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -24,14 +24,16 @@ { "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 } ], "qoraHoldersShare": 0.20, "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 }