diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip index 06643cca..3cc97a79 100755 --- a/WindowsInstaller/Qortal.aip +++ b/WindowsInstaller/Qortal.aip @@ -17,10 +17,10 @@ - + - + @@ -212,7 +212,7 @@ - + diff --git a/pom.xml b/pom.xml index e951c7c7..4d0e0f23 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 3.4.0 + 3.4.2 jar true diff --git a/src/main/java/org/qortal/ApplyUpdate.java b/src/main/java/org/qortal/ApplyUpdate.java index 796bf580..7a870460 100644 --- a/src/main/java/org/qortal/ApplyUpdate.java +++ b/src/main/java/org/qortal/ApplyUpdate.java @@ -8,6 +8,7 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.security.Security; import java.util.*; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,6 +19,8 @@ import org.qortal.api.ApiRequest; import org.qortal.controller.AutoUpdate; import org.qortal.settings.Settings; +import static org.qortal.controller.AutoUpdate.AGENTLIB_JVM_HOLDER_ARG; + public class ApplyUpdate { static { @@ -197,6 +200,11 @@ public class ApplyUpdate { // JVM arguments javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + // Reapply any retained, but disabled, -agentlib JVM arg + javaCmd = javaCmd.stream() + .map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib")) + .collect(Collectors.toList()); + // Call mainClass in JAR javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME)); @@ -205,7 +213,7 @@ public class ApplyUpdate { } try { - LOGGER.info(() -> String.format("Restarting node with: %s", String.join(" ", javaCmd))); + LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); @@ -214,8 +222,15 @@ public class ApplyUpdate { processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE); } - processBuilder.start(); - } catch (IOException e) { + // New process will inherit our stdout and stderr + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = processBuilder.start(); + + // Nothing to pipe to new process, so close output stream (process's stdin) + process.getOutputStream().close(); + } catch (Exception e) { LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage())); } } diff --git a/src/main/java/org/qortal/api/resource/BlocksResource.java b/src/main/java/org/qortal/api/resource/BlocksResource.java index 28d54a44..195b2ca4 100644 --- a/src/main/java/org/qortal/api/resource/BlocksResource.java +++ b/src/main/java/org/qortal/api/resource/BlocksResource.java @@ -114,7 +114,7 @@ public class BlocksResource { @Path("/signature/{signature}/data") @Operation( summary = "Fetch serialized, base58 encoded block data using base58 signature", - description = "Returns serialized data for the block that matches the given signature", + description = "Returns serialized data for the block that matches the given signature, and an optional block serialization version", responses = { @ApiResponse( description = "the block data", @@ -125,7 +125,7 @@ public class BlocksResource { @ApiErrors({ ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE }) - public String getSerializedBlockData(@PathParam("signature") String signature58) { + public String getSerializedBlockData(@PathParam("signature") String signature58, @QueryParam("version") Integer version) { // Decode signature byte[] signature; try { @@ -136,20 +136,41 @@ public class BlocksResource { try (final Repository repository = RepositoryManager.getRepository()) { + // Default to version 1 + if (version == null) { + version = 1; + } + // Check the database first BlockData blockData = repository.getBlockRepository().fromSignature(signature); if (blockData != null) { Block block = new Block(repository, blockData); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); bytes.write(Ints.toByteArray(block.getBlockData().getHeight())); - bytes.write(BlockTransformer.toBytes(block)); + + switch (version) { + case 1: + bytes.write(BlockTransformer.toBytes(block)); + break; + + case 2: + bytes.write(BlockTransformer.toBytesV2(block)); + break; + + default: + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + return Base58.encode(bytes.toByteArray()); } // Not found, so try the block archive byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository); if (bytes != null) { - return Base58.encode(bytes); + if (version != 1) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Archived blocks require version 1"); + } + return Base58.encode(bytes); } throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN); 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 8bbefb11..cddb38cc 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -68,6 +68,7 @@ public class BlockChain { atFindNextTransactionFix, newBlockSigHeight, shareBinFix, + rewardShareLimitTimestamp, calcChainWeightTimestamp, transactionV5Timestamp, transactionV6Timestamp, @@ -103,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 */ @@ -120,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. *

@@ -157,7 +177,7 @@ public class BlockChain { private int minAccountLevelToMint; private int minAccountLevelForBlockSubmissions; private int minAccountLevelToRewardShare; - private int maxRewardSharesPerMintingAccount; + private int maxRewardSharesPerFounderMintingAccount; private int founderEffectiveMintingLevel; /** Minimum time to retain online account signatures (ms) for block validity checks. */ @@ -169,6 +189,13 @@ public class BlockChain { * featureTriggers because unit tests need to set this value via Reflection. */ private long onlineAccountsModulusV2Timestamp; + /** Max reward shares by block height */ + public static class MaxRewardSharesByTimestamp { + public long timestamp; + public int maxShares; + } + private List maxRewardSharesByTimestamp; + /** Settings relating to CIYAM AT feature. */ public static class CiyamAtSettings { /** Fee per step/op-code executed. */ @@ -363,6 +390,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; } @@ -375,8 +410,8 @@ public class BlockChain { return this.minAccountLevelToRewardShare; } - public int getMaxRewardSharesPerMintingAccount() { - return this.maxRewardSharesPerMintingAccount; + public int getMaxRewardSharesPerFounderMintingAccount() { + return this.maxRewardSharesPerFounderMintingAccount; } public int getFounderEffectiveMintingLevel() { @@ -409,6 +444,10 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue(); } + public long getRewardShareLimitTimestamp() { + return this.featureTriggers.get(FeatureTrigger.rewardShareLimitTimestamp.name()).longValue(); + } + public long getCalcChainWeightTimestamp() { return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue(); } @@ -457,6 +496,14 @@ public class BlockChain { return this.getUnitFee(); } + public int getMaxRewardSharesAtTimestamp(long ourTimestamp) { + for (int i = maxRewardSharesByTimestamp.size() - 1; i >= 0; --i) + if (maxRewardSharesByTimestamp.get(i).timestamp <= ourTimestamp) + return maxRewardSharesByTimestamp.get(i).maxShares; + + return 0; + } + /** Validate blockchain config read from JSON */ private void validateConfig() { if (this.genesisInfo == null) diff --git a/src/main/java/org/qortal/controller/AutoUpdate.java b/src/main/java/org/qortal/controller/AutoUpdate.java index f07e82d1..2ec7c94a 100644 --- a/src/main/java/org/qortal/controller/AutoUpdate.java +++ b/src/main/java/org/qortal/controller/AutoUpdate.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -40,6 +41,7 @@ public class AutoUpdate extends Thread { public static final String JAR_FILENAME = "qortal.jar"; public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME; + public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib="; private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class); private static final long CHECK_INTERVAL = 20 * 60 * 1000L; // ms @@ -243,6 +245,11 @@ public class AutoUpdate extends Thread { // JVM arguments javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port + javaCmd = javaCmd.stream() + .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG)) + .collect(Collectors.toList()); + // Remove JNI options as they won't be supported by command-line 'java' // These are typically added by the AdvancedInstaller Java launcher EXE javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf")); @@ -261,10 +268,19 @@ public class AutoUpdate extends Thread { Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"), MessageType.INFO); - new ProcessBuilder(javaCmd).start(); + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + // New process will inherit our stdout and stderr + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = processBuilder.start(); + + // Nothing to pipe to new process, so close output stream (process's stdin) + process.getOutputStream().close(); return true; // applying update OK - } catch (IOException e) { + } catch (Exception e) { LOGGER.error(String.format("Failed to apply update: %s", e.getMessage())); try { diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 2d736e76..343ab4af 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -90,37 +90,40 @@ public class BlockMinter extends Thread { List newBlocks = new ArrayList<>(); - // Flags for tracking change in whether minting is possible, - // so we can notify Controller, and further update SysTray, etc. - boolean isMintingPossible = false; - boolean wasMintingPossible = isMintingPossible; - while (running) { - if (isMintingPossible != wasMintingPossible) - Controller.getInstance().onMintingPossibleChange(isMintingPossible); + try (final Repository repository = RepositoryManager.getRepository()) { + // Going to need this a lot... + BlockRepository blockRepository = repository.getBlockRepository(); - wasMintingPossible = isMintingPossible; + // Flags for tracking change in whether minting is possible, + // so we can notify Controller, and further update SysTray, etc. + boolean isMintingPossible = false; + boolean wasMintingPossible = isMintingPossible; + while (running) { + if (isMintingPossible != wasMintingPossible) + Controller.getInstance().onMintingPossibleChange(isMintingPossible); - try { - // Sleep for a while - Thread.sleep(1000); + wasMintingPossible = isMintingPossible; - isMintingPossible = false; + try { + // Free up any repository locks + repository.discardChanges(); - final Long now = NTP.getTime(); - if (now == null) - continue; + // Sleep for a while + Thread.sleep(1000); - final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); - if (minLatestBlockTimestamp == null) - continue; + isMintingPossible = false; - // No online accounts for current timestamp? (e.g. during startup) - if (!OnlineAccountsManager.getInstance().hasOnlineAccounts()) - continue; + final Long now = NTP.getTime(); + if (now == null) + continue; - try (final Repository repository = RepositoryManager.getRepository()) { - // Going to need this a lot... - BlockRepository blockRepository = repository.getBlockRepository(); + final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); + if (minLatestBlockTimestamp == null) + continue; + + // No online accounts for current timestamp? (e.g. during startup) + if (!OnlineAccountsManager.getInstance().hasOnlineAccounts()) + continue; List mintingAccountsData = repository.getAccountRepository().getMintingAccounts(); // No minting accounts? @@ -198,10 +201,6 @@ public class BlockMinter extends Thread { // so go ahead and mint a block if possible. isMintingPossible = true; - // Reattach newBlocks to new repository handle - for (Block newBlock : newBlocks) - newBlock.setRepository(repository); - // Check blockchain hasn't changed if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) { previousBlockData = lastBlockData; @@ -439,13 +438,13 @@ public class BlockMinter extends Thread { Network network = Network.getInstance(); network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData)); } - } catch (DataException e) { - LOGGER.warn("Repository issue while running block minter", e); + } catch (InterruptedException e) { + // We've been interrupted - time to exit + return; } - } catch (InterruptedException e) { - // We've been interrupted - time to exit - return; } + } catch (DataException e) { + LOGGER.warn("Repository issue while running block minter - NO LONGER MINTING", e); } } diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index cde965c1..2bf01de3 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -819,7 +819,7 @@ public class Controller extends Thread { actionText = String.format("%s", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN")); SysTray.getInstance().setTrayIcon(3); } - else if (OnlineAccountsManager.getInstance().hasOnlineAccounts()) { + else if (OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures()) { actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED"); SysTray.getInstance().setTrayIcon(2); } diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java index 529cc853..05fb7f29 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -480,7 +480,7 @@ public class OnlineAccountsManager { final Long minLatestBlockTimestamp = NTP.getTime() - (2 * 60 * 60 * 1000L); boolean isUpToDate = Controller.getInstance().isUpToDate(minLatestBlockTimestamp); - return isUpToDate && hasOnlineAccounts(); + return isUpToDate && hasOurOnlineAccounts(); } public boolean hasOurOnlineAccounts() { diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index c1d31e31..281f34f1 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -159,6 +159,9 @@ public interface AccountRepository { /** Returns number of active reward-shares involving passed public key as the minting account only. */ public int countRewardShares(byte[] mintingAccountPublicKey) throws DataException; + /** Returns number of active self-shares involving passed public key as the minting account only. */ + public int countSelfShares(byte[] mintingAccountPublicKey) throws DataException; + public List getRewardShares() throws DataException; public List findRewardShares(List mintingAccounts, List recipientAccounts, List involvedAddresses, Integer limit, Integer offset, Boolean reverse) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 028f3d46..9fdb0a3f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -688,6 +688,17 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public int countSelfShares(byte[] minterPublicKey) throws DataException { + String sql = "SELECT COUNT(*) FROM RewardShares WHERE minter_public_key = ? AND minter = recipient"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, minterPublicKey)) { + return resultSet.getInt(1); + } catch (SQLException e) { + throw new DataException("Unable to count self-shares in repository", e); + } + } + @Override public List getRewardShares() throws DataException { String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares"; diff --git a/src/main/java/org/qortal/transaction/RewardShareTransaction.java b/src/main/java/org/qortal/transaction/RewardShareTransaction.java index be68196d..ed5029b2 100644 --- a/src/main/java/org/qortal/transaction/RewardShareTransaction.java +++ b/src/main/java/org/qortal/transaction/RewardShareTransaction.java @@ -140,8 +140,21 @@ public class RewardShareTransaction extends Transaction { // Check the minting account hasn't reach maximum number of reward-shares int rewardShareCount = this.repository.getAccountRepository().countRewardShares(creator.getPublicKey()); - if (rewardShareCount >= BlockChain.getInstance().getMaxRewardSharesPerMintingAccount()) + int selfShareCount = this.repository.getAccountRepository().countSelfShares(creator.getPublicKey()); + + int maxRewardShares = BlockChain.getInstance().getMaxRewardSharesAtTimestamp(this.rewardShareTransactionData.getTimestamp()); + if (creator.isFounder()) + // Founders have a different limit + maxRewardShares = BlockChain.getInstance().getMaxRewardSharesPerFounderMintingAccount(); + + if (rewardShareCount >= maxRewardShares) return ValidationResult.MAXIMUM_REWARD_SHARES; + + // When filling all reward share slots, one must be a self share (after feature trigger timestamp) + if (this.rewardShareTransactionData.getTimestamp() >= BlockChain.getInstance().getRewardShareLimitTimestamp()) + if (!isRecipientAlsoMinter && rewardShareCount == maxRewardShares-1 && selfShareCount == 0) + return ValidationResult.MAXIMUM_REWARD_SHARES; + } else { // This transaction intends to modify/terminate an existing reward-share diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index dc84ebd0..670027d5 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -15,7 +15,11 @@ "minAccountLevelToMint": 1, "minAccountLevelForBlockSubmissions": 5, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 6, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 1657382400000, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 43200000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -36,14 +40,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 } @@ -58,6 +64,7 @@ "atFindNextTransactionFix": 275000, "newBlockSigHeight": 320000, "shareBinFix": 399000, + "rewardShareLimitTimestamp": 1657382400000, "calcChainWeightTimestamp": 1620579600000, "transactionV5Timestamp": 1642176000000, "transactionV6Timestamp": 9999999999999, diff --git a/src/test/java/org/qortal/test/common/AccountUtils.java b/src/test/java/org/qortal/test/common/AccountUtils.java index bda1ae61..ffc4a7a1 100644 --- a/src/test/java/org/qortal/test/common/AccountUtils.java +++ b/src/test/java/org/qortal/test/common/AccountUtils.java @@ -41,7 +41,10 @@ public class AccountUtils { public static TransactionData createRewardShare(Repository repository, String minter, String recipient, int sharePercent) throws DataException { PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, minter); PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient); + return createRewardShare(repository, mintingAccount, recipientAccount, sharePercent); + } + public static TransactionData createRewardShare(Repository repository, PrivateKeyAccount mintingAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException { byte[] reference = mintingAccount.getLastReference(); long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1; @@ -66,6 +69,15 @@ public class AccountUtils { return rewardSharePrivateKey; } + public static byte[] rewardShare(Repository repository, PrivateKeyAccount minterAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException { + TransactionData transactionData = createRewardShare(repository, minterAccount, recipientAccount, sharePercent); + + TransactionUtils.signAndMint(repository, transactionData, minterAccount); + byte[] rewardSharePrivateKey = minterAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey()); + + return rewardSharePrivateKey; + } + public static Map> getBalances(Repository repository, long... assetIds) throws DataException { Map> balances = new HashMap<>(); diff --git a/src/test/java/org/qortal/test/common/Common.java b/src/test/java/org/qortal/test/common/Common.java index cb782343..3270a795 100644 --- a/src/test/java/org/qortal/test/common/Common.java +++ b/src/test/java/org/qortal/test/common/Common.java @@ -7,6 +7,7 @@ import java.math.BigDecimal; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.SecureRandom; import java.security.Security; import java.util.ArrayList; import java.util.Collections; @@ -25,6 +26,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.qortal.account.PrivateKeyAccount; import org.qortal.block.BlockChain; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.asset.AssetData; @@ -111,6 +113,12 @@ public class Common { return testAccountsByName.values().stream().map(account -> new TestAccount(repository, account)).collect(Collectors.toList()); } + public static PrivateKeyAccount generateRandomSeedAccount(Repository repository) { + byte[] seed = new byte[32]; + new SecureRandom().nextBytes(seed); + return new PrivateKeyAccount(repository, seed); + } + public static void useSettingsAndDb(String settingsFilename, boolean dbInMemory) throws DataException { closeRepository(); diff --git a/src/test/java/org/qortal/test/minting/RewardShareTests.java b/src/test/java/org/qortal/test/minting/RewardShareTests.java index cde3c2ff..b5ac5e59 100644 --- a/src/test/java/org/qortal/test/minting/RewardShareTests.java +++ b/src/test/java/org/qortal/test/minting/RewardShareTests.java @@ -177,4 +177,143 @@ public class RewardShareTests extends Common { } } + @Test + public void testCreateRewardSharesBeforeReduction() throws DataException { + final int sharePercent = 0; + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Create 6 reward shares + for (int i=0; i<6; i++) { + AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } + + // 7th reward share should fail because we've reached the limit (and we're not yet requiring a self share) + AssertionError assertionError = null; + try { + AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } catch (AssertionError e) { + assertionError = e; + } + assertNotNull("Transaction should be invalid", assertionError); + assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES")); + } + } + + @Test + public void testCreateRewardSharesAfterReduction() throws DataException { + Common.useSettings("test-settings-v2-reward-shares.json"); + + final int sharePercent = 0; + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Create 2 reward shares + for (int i=0; i<2; i++) { + AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } + + // 3rd reward share should fail because we've reached the limit (and we haven't got a self share) + AssertionError assertionError = null; + try { + AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } catch (AssertionError e) { + assertionError = e; + } + assertNotNull("Transaction should be invalid", assertionError); + assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES")); + } + } + + @Test + public void testCreateSelfAndRewardSharesAfterReduction() throws DataException { + Common.useSettings("test-settings-v2-reward-shares.json"); + + final int sharePercent = 0; + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Create 2 reward shares + for (int i=0; i<2; i++) { + AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } + + // 3rd reward share should fail because we've reached the limit (and we haven't got a self share) + AssertionError assertionError = null; + try { + AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } catch (AssertionError e) { + assertionError = e; + } + assertNotNull("Transaction should be invalid", assertionError); + assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES")); + + // Now create a self share, which should succeed as we have space for it + AccountUtils.rewardShare(repository, dilbertAccount, dilbertAccount, sharePercent); + + // 4th reward share should fail because we've reached the limit (including the self share) + assertionError = null; + try { + AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } catch (AssertionError e) { + assertionError = e; + } + assertNotNull("Transaction should be invalid", assertionError); + assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES")); + } + } + + @Test + public void testCreateFounderRewardSharesBeforeReduction() throws DataException { + final int sharePercent = 0; + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceFounderAccount = Common.getTestAccount(repository, "alice"); + + // Create 5 reward shares (not 6, because alice already starts with a self reward share in the genesis block) + for (int i=0; i<5; i++) { + AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } + + // 6th reward share should fail + AssertionError assertionError = null; + try { + AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } catch (AssertionError e) { + assertionError = e; + } + assertNotNull("Transaction should be invalid", assertionError); + assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES")); + } + } + + @Test + public void testCreateFounderRewardSharesAfterReduction() throws DataException { + Common.useSettings("test-settings-v2-reward-shares.json"); + + final int sharePercent = 0; + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceFounderAccount = Common.getTestAccount(repository, "alice"); + + // Create 5 reward shares (not 6, because alice already starts with a self reward share in the genesis block) + for (int i=0; i<5; i++) { + AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } + + // 6th reward share should fail + AssertionError assertionError = null; + try { + AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent); + } catch (AssertionError e) { + assertionError = e; + } + assertNotNull("Transaction should be invalid", assertionError); + assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES")); + } + } + } 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 3fa8a1e5..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 }, @@ -52,6 +54,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 9999999999999, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 64aecbb6..3ad7da60 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -20,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 } @@ -51,6 +57,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 30b1ffdd..c9d63800 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index bb94231a..ad5a1f9a 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 4c1c51b9..1d57e119 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, 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 cfeac824..97d7a320 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 71d05767..6d0c2223 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index eda61338..03a05a5e 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 20 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 6, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index dafd3a51..a108c651 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 20 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json new file mode 100644 index 00000000..8f14e48f --- /dev/null +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -0,0 +1,93 @@ +{ + "isTestChain": true, + "blockTimestampMargin": 500, + "transactionExpiryPeriod": 86400000, + "maxBlockSize": 2097152, + "maxBytesPerUnitFee": 1024, + "unitFee": "0.1", + "nameRegistrationUnitFees": [ + { "timestamp": 1645372800000, "fee": "5" } + ], + "requireGroupForApproval": false, + "minAccountLevelToRewardShare": 5, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 1655460000000, "maxShares": 3 } + ], + "founderEffectiveMintingLevel": 10, + "onlineAccountSignaturesMinLifetime": 3600000, + "onlineAccountSignaturesMaxLifetime": 86400000, + "rewardsByHeight": [ + { "height": 1, "reward": 100 }, + { "height": 11, "reward": 10 }, + { "height": 21, "reward": 1 } + ], + "sharesByLevel": [ + { "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 } + ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, + "featureTriggers": { + "messageHeight": 0, + "atHeight": 0, + "assetsTimestamp": 0, + "votingTimestamp": 0, + "arbitraryTimestamp": 0, + "powfixTimestamp": 0, + "qortalTimestamp": 0, + "newAssetPricingTimestamp": 0, + "groupApprovalTimestamp": 0, + "atFindNextTransactionFix": 0, + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "rewardShareLimitTimestamp": 0, + "calcChainWeightTimestamp": 0, + "newConsensusTimestamp": 0, + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0, + "disableReferenceTimestamp": 9999999999999, + "aggregateSignatureTimestamp": 0 + }, + "genesisInfo": { + "version": 4, + "timestamp": 0, + "transactions": [ + { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + { "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + + { "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" }, + { "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" }, + + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + + { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": "100" }, + + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 } + ] + } +} diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 114cf91e..3b380d29 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -10,7 +10,11 @@ ], "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, - "maxRewardSharesPerMintingAccount": 20, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -21,14 +25,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 } @@ -52,6 +58,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, diff --git a/src/test/resources/test-settings-v2-reward-shares.json b/src/test/resources/test-settings-v2-reward-shares.json new file mode 100644 index 00000000..092ebcc8 --- /dev/null +++ b/src/test/resources/test-settings-v2-reward-shares.json @@ -0,0 +1,19 @@ +{ + "repositoryPath": "testdb", + "bitcoinNet": "TEST3", + "litecoinNet": "TEST3", + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-reward-shares.json", + "exportPath": "qortal-backup-test", + "bootstrap": false, + "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, + "minPeers": 0, + "pruneBlockLimit": 100, + "bootstrapFilenamePrefix": "test-", + "dataPath": "data-test", + "tempDataPath": "data-test/_temp", + "listsPath": "lists-test", + "storagePolicy": "FOLLOWED_OR_VIEWED", + "maxStorageCapacity": 104857600 +}