From 10a578428bbd97c67af546da82ec5e4cba529c06 Mon Sep 17 00:00:00 2001 From: catbref Date: Sun, 19 Jun 2022 18:25:00 +0100 Subject: [PATCH 01/14] Improve intermittent auto-update failures, mostly under non-Windows environments. Symptoms are: * AutoUpdate trying to run new ApplyUpdate process, but nothing appears in log-apply-update.?.txt * Main qortal.jar process continues to run without updating * Last AutoUpdate line in log.txt.? is: 2022-06-18 15:42:46 INFO AutoUpdate:258 - Applying update with: /usr/local/openjdk11/bin/java -Djava.net.preferIPv4Stack=false -Xss256k -Xmx1024m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:5005 -cp new-qortal.jar org.qortal.ApplyUpdate Changes are: * child process now inherits parent's stdout / stderr (was piped from parent) * child process is given a fresh stdin, which is immediately closed * AutoUpdate now converts -agentlib JVM arg to -DQORTAL_agentlib * ApplyUpdate converts -DQORTAL_agentlib to -agentlib The latter two changes are to prevent a conflict where two processes try to reuse the same JVM debugging port number. --- src/main/java/org/qortal/ApplyUpdate.java | 21 ++++++++++++++++--- .../org/qortal/controller/AutoUpdate.java | 20 ++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) 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/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 { From 08f3351a7adfd254db28aed718dae673036ae7a2 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 1 Jul 2022 12:18:48 +0100 Subject: [PATCH 02/14] Reward share transaction modifications: - Reduce concurrent reward share limit from 6 to 3 (or from 5 to 2 when including self share) - as per community vote. - Founders remain at 6 (5 when including self share) - also decided in community vote. - When all slots are being filled, require that at least one is a self share, so that not all can be used for sponsorship. - Activates at future undecided timestamp. --- .../java/org/qortal/block/BlockChain.java | 26 +++- .../qortal/repository/AccountRepository.java | 3 + .../hsqldb/HSQLDBAccountRepository.java | 11 ++ .../transaction/RewardShareTransaction.java | 15 +- src/main/resources/blockchain.json | 7 +- .../org/qortal/test/common/AccountUtils.java | 12 ++ .../java/org/qortal/test/common/Common.java | 8 + .../qortal/test/minting/RewardShareTests.java | 139 ++++++++++++++++++ .../test-chain-v2-block-timestamps.json | 1 + .../test-chain-v2-disable-reference.json | 7 +- .../test-chain-v2-founder-rewards.json | 7 +- .../test-chain-v2-leftover-reward.json | 7 +- src/test/resources/test-chain-v2-minting.json | 7 +- .../test-chain-v2-qora-holder-extremes.json | 7 +- .../resources/test-chain-v2-qora-holder.json | 7 +- .../test-chain-v2-reward-levels.json | 7 +- .../test-chain-v2-reward-scaling.json | 7 +- .../test-chain-v2-reward-shares.json | 91 ++++++++++++ src/test/resources/test-chain-v2.json | 7 +- .../test-settings-v2-reward-shares.json | 19 +++ 20 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 src/test/resources/test-chain-v2-reward-shares.json create mode 100644 src/test/resources/test-settings-v2-reward-shares.json diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index e0e64e24..1dbc9a23 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, @@ -157,7 +158,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. */ @@ -165,6 +166,13 @@ public class BlockChain { /** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */ private long onlineAccountSignaturesMaxLifetime; + /** 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. */ @@ -366,8 +374,8 @@ public class BlockChain { return this.minAccountLevelToRewardShare; } - public int getMaxRewardSharesPerMintingAccount() { - return this.maxRewardSharesPerMintingAccount; + public int getMaxRewardSharesPerFounderMintingAccount() { + return this.maxRewardSharesPerFounderMintingAccount; } public int getFounderEffectiveMintingLevel() { @@ -400,6 +408,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(); } @@ -448,6 +460,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/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 8f9b62d4..7cfafba4 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": 9999999999999, "maxShares": 3 } + ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 43200000, "onlineAccountSignaturesMaxLifetime": 86400000, @@ -57,6 +61,7 @@ "atFindNextTransactionFix": 275000, "newBlockSigHeight": 320000, "shareBinFix": 399000, + "rewardShareLimitTimestamp": 9999999999999, "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/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 3fa8a1e5..38a18a8c 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -52,6 +52,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..648e91b5 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, @@ -51,6 +55,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 8ccac3c5..540d7efd 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, @@ -51,6 +55,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 347d984e..ffd81379 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, @@ -51,6 +55,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 f81693e6..8d66e072 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, @@ -51,6 +55,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 d6e8f24d..9e8ff2a8 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, @@ -51,6 +55,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 e5b7945e..0dac2457 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, @@ -51,6 +55,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 664b170b..90d201a3 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, @@ -51,6 +55,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 65a3604d..6b2cbc0c 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, @@ -51,6 +55,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..9e713095 --- /dev/null +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -0,0 +1,91 @@ +{ + "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": [ + { "levels": [ 1, 2 ], "share": 0.05 }, + { "levels": [ 3, 4 ], "share": 0.10 }, + { "levels": [ 5, 6 ], "share": 0.15 }, + { "levels": [ 7, 8 ], "share": 0.20 }, + { "levels": [ 9, 10 ], "share": 0.25 } + ], + "qoraHoldersShare": 0.20, + "qoraPerQortReward": 250, + "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], + "blockTimingsByHeight": [ + { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } + ], + "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 267a49a0..c08dac04 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, @@ -51,6 +55,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 +} From d7e7c1f48ce0b91996cfb3652956002b3a39a201 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 2 Jul 2022 10:33:00 +0100 Subject: [PATCH 03/14] Fixed bugs from merge conflict, causing incorrect systray statuses in some cases. --- src/main/java/org/qortal/controller/Controller.java | 2 +- src/main/java/org/qortal/controller/OnlineAccountsManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 1e476a4a..9a82df00 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -471,7 +471,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() { From 6b91b0477df744399f1625180824b7b6d65d8929 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Mon, 4 Jul 2022 19:57:54 +0100 Subject: [PATCH 04/14] Added version query string param to /blocks/signature/{signature}/data API endpoint, to allow for optional V2 block serialization (with a single combined AT states hash). Version can only be specified when querying unarchived blocks; archived blocks require V1 for now (and possibly V2 in the future). --- .../qortal/api/resource/BlocksResource.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) 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); From a8b58d2007296305c56a85df208b573478b957d2 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 5 Jul 2022 20:34:23 +0100 Subject: [PATCH 05/14] Reward share limit activation timestamp set to 1657382400000 (Sat Jul 09 2022 16:00:00 UTC) --- src/main/resources/blockchain.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 7cfafba4..b65fd72e 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -18,7 +18,7 @@ "maxRewardSharesPerFounderMintingAccount": 6, "maxRewardSharesByTimestamp": [ { "timestamp": 0, "maxShares": 6 }, - { "timestamp": 9999999999999, "maxShares": 3 } + { "timestamp": 1657382400000, "maxShares": 3 } ], "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 43200000, @@ -61,7 +61,7 @@ "atFindNextTransactionFix": 275000, "newBlockSigHeight": 320000, "shareBinFix": 399000, - "rewardShareLimitTimestamp": 9999999999999, + "rewardShareLimitTimestamp": 1657382400000, "calcChainWeightTimestamp": 1620579600000, "transactionV5Timestamp": 1642176000000, "transactionV6Timestamp": 9999999999999, From ad0d8fac91cfe0d0d496c86f709e864842e79afb Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 5 Jul 2022 20:56:40 +0100 Subject: [PATCH 06/14] Bump version to 3.4.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e951c7c7..9e7c9741 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 3.4.0 + 3.4.1 jar true From 90e8cfc737bf38f8104277a3aa0e1323dfcfdf64 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 8 Jul 2022 11:12:58 +0100 Subject: [PATCH 07/14] qoraHoldersShare reworked to qoraHoldersShareByHeight. This allows the QORA share percentage to be modified at different heights, based on community votes. Added unit test to simulate a reduction. --- src/main/java/org/qortal/block/Block.java | 2 +- .../java/org/qortal/block/BlockChain.java | 30 ++++++++---- src/main/resources/blockchain.json | 5 +- .../org/qortal/test/minting/RewardTests.java | 47 ++++++++++++++++++- .../test-chain-v2-block-timestamps.json | 5 +- .../test-chain-v2-disable-reference.json | 5 +- .../test-chain-v2-founder-rewards.json | 5 +- .../test-chain-v2-leftover-reward.json | 5 +- src/test/resources/test-chain-v2-minting.json | 5 +- .../test-chain-v2-qora-holder-extremes.json | 5 +- .../resources/test-chain-v2-qora-holder.json | 5 +- .../test-chain-v2-reward-levels.json | 5 +- .../test-chain-v2-reward-scaling.json | 5 +- .../test-chain-v2-reward-shares.json | 5 +- src/test/resources/test-chain-v2.json | 5 +- 15 files changed, 114 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index ddfe247a..bbc6e31b 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1914,7 +1914,7 @@ public class Block { // Fetch list of legacy QORA holders who haven't reached their cap of QORT reward. List qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight()); final boolean haveQoraHolders = !qoraHolders.isEmpty(); - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight()); // Perform account-level-based reward scaling if appropriate if (!haveFounders) { diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 1dbc9a23..239ebaa2 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -113,9 +113,13 @@ public class BlockChain { /** Generated lookup of share-bin by account level */ private AccountLevelShareBin[] shareBinsByLevel; - /** Share of block reward/fees to legacy QORA coin holders */ - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private Long qoraHoldersShare; + /** Share of block reward/fees to legacy QORA coin holders, by block height */ + public static class ShareByHeight { + public int height; + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long share; + } + private List qoraHoldersShareByHeight; /** How many legacy QORA per 1 QORT of block reward. */ @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @@ -354,10 +358,6 @@ public class BlockChain { return this.cumulativeBlocksByLevel; } - public long getQoraHoldersShare() { - return this.qoraHoldersShare; - } - public long getQoraPerQortReward() { return this.qoraPerQortReward; } @@ -468,6 +468,15 @@ public class BlockChain { return 0; } + public long getQoraHoldersShareAtHeight(int ourHeight) { + // Scan through for QORA share at our height + for (int i = qoraHoldersShareByHeight.size() - 1; i >= 0; --i) + if (qoraHoldersShareByHeight.get(i).height <= ourHeight) + return qoraHoldersShareByHeight.get(i).share; + + return 0; + } + /** Validate blockchain config read from JSON */ private void validateConfig() { if (this.genesisInfo == null) @@ -479,8 +488,8 @@ public class BlockChain { if (this.sharesByLevel == null) Settings.throwValidationError("No \"sharesByLevel\" entry found in blockchain config"); - if (this.qoraHoldersShare == null) - Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config"); + if (this.qoraHoldersShareByHeight == null) + Settings.throwValidationError("No \"qoraHoldersShareByHeight\" entry found in blockchain config"); if (this.qoraPerQortReward == null) Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config"); @@ -518,7 +527,7 @@ public class BlockChain { Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name())); // Check block reward share bounds - long totalShare = this.qoraHoldersShare; + long totalShare = this.getQoraHoldersShareAtHeight(1); // Add share percents for account-level-based rewards for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel) totalShare += accountLevelShareBin.share; @@ -556,6 +565,7 @@ public class BlockChain { this.blocksNeededByLevel = Collections.unmodifiableList(this.blocksNeededByLevel); this.cumulativeBlocksByLevel = Collections.unmodifiableList(this.cumulativeBlocksByLevel); this.blockTimingsByHeight = Collections.unmodifiableList(this.blockTimingsByHeight); + this.qoraHoldersShareByHeight = Collections.unmodifiableList(this.qoraHoldersShareByHeight); } /** diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index b65fd72e..9f9d3a2b 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -45,7 +45,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 9999999, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ], "blockTimingsByHeight": [ diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index f7970ace..658f285f 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -4,6 +4,7 @@ import static org.junit.Assert.*; import java.math.BigInteger; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -14,6 +15,7 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.block.BlockChain.RewardByHeight; import org.qortal.controller.BlockMinter; @@ -109,7 +111,7 @@ public class RewardTests extends Common { public void testLegacyQoraReward() throws DataException { Common.useSettings("test-settings-v2-qora-holder-extremes.json"); - long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); BigInteger qoraHoldersShareBI = BigInteger.valueOf(qoraHoldersShare); long qoraPerQort = BlockChain.getInstance().getQoraPerQortReward(); @@ -190,6 +192,47 @@ public class RewardTests extends Common { } } + @Test + public void testLegacyQoraRewardReduction() throws DataException { + Common.useSettings("test-settings-v2-qora-holder-extremes.json"); + + // Make sure that the QORA share reduces between blocks 4 and 5 + assertTrue(BlockChain.getInstance().getQoraHoldersShareAtHeight(5) < BlockChain.getInstance().getQoraHoldersShareAtHeight(4)); + + // Keep track of balance deltas at each height + Map chloeQortBalanceDeltaAtEachHeight = new HashMap<>(); + + try (final Repository repository = RepositoryManager.getRepository()) { + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + long chloeLastQortBalance = initialBalances.get("chloe").get(Asset.QORT); + + for (int i=2; i<=10; i++) { + + Block block = BlockUtils.mintBlock(repository); + + // Add to map of balance deltas at each height + long chloeNewQortBalance = AccountUtils.getBalance(repository, "chloe", Asset.QORT); + chloeQortBalanceDeltaAtEachHeight.put(block.getBlockData().getHeight(), chloeNewQortBalance - chloeLastQortBalance); + chloeLastQortBalance = chloeNewQortBalance; + } + + // Ensure blocks 2-4 paid out the same rewards to Chloe + assertEquals(chloeQortBalanceDeltaAtEachHeight.get(2), chloeQortBalanceDeltaAtEachHeight.get(4)); + + // Ensure block 5 paid a lower reward + assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) < chloeQortBalanceDeltaAtEachHeight.get(4)); + + // Check that the reward was 20x lower + assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) == chloeQortBalanceDeltaAtEachHeight.get(4) / 20); + + // Orphan to block 4 and ensure that Chloe's balance hasn't been incorrectly affected by the reward reduction + BlockUtils.orphanToBlock(repository, 4); + long expectedChloeQortBalance = initialBalances.get("chloe").get(Asset.QORT) + chloeQortBalanceDeltaAtEachHeight.get(2) + + chloeQortBalanceDeltaAtEachHeight.get(3) + chloeQortBalanceDeltaAtEachHeight.get(4); + assertEquals(expectedChloeQortBalance, AccountUtils.getBalance(repository, "chloe", Asset.QORT)); + } + } + /** Use Alice-Chloe reward-share to bump Chloe from level 0 to level 1, then check orphaning works as expected. */ @Test public void testLevel1() throws DataException { @@ -295,7 +338,7 @@ public class RewardTests extends Common { * So Dilbert should receive 100% - legacy QORA holder's share. */ - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); final long remainingShare = 1_00000000 - qoraHoldersShare; long dilbertExpectedBalance = initialBalances.get("dilbert").get(Asset.QORT); diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 38a18a8c..782f6152 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -26,7 +26,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 648e91b5..633d8aa4 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 540d7efd..f4d39517 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index ffd81379..e2578260 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 8d66e072..d1ea3992 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 9e8ff2a8..da6f25d9 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 5, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 0dac2457..9d4f1777 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 90d201a3..949ae5c0 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 6b2cbc0c..2a7d830b 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 9e713095..4b800c83 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index c08dac04..832be222 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -30,7 +30,10 @@ { "levels": [ 7, 8 ], "share": 0.20 }, { "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], "blockTimingsByHeight": [ From 35f343068710ff28a5f2b99722c6d15046e8f896 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 10 Jul 2022 12:09:44 +0100 Subject: [PATCH 08/14] Added share bin activation feature. To prevent a single or very small number of minters receiving the rewards for an entire tier, share bins can now require "activation". This adds the requirement that a minimum number of accounts must be present in a share bin before it is considered active. When inactive, the rewards and minters are added to the previous tier. Summary of new functionality: - If a share bin has more than one, but less than 30 accounts present, the rewards and accounts are shifted to the previous share bin. - This process is iterative, so the accounts can shift through multiple tiers until the minimum number of accounts is met, OR the share bin's starting level is less than shareBinActivationMinLevel. - Applies to level 7+, so that no backwards support is needed. It will only take effect once the first account reaches level 7. This requires hot swapping the sharesByLevel data to combine tiers where needed, so is a considerable shift away from the immutable array that was in place previously. All existing and new unit tests are now passing, however a lot more testing will be needed. --- src/main/java/org/qortal/block/Block.java | 70 ++++++- .../java/org/qortal/block/BlockChain.java | 29 ++- src/main/resources/blockchain.json | 12 +- .../org/qortal/test/minting/RewardTests.java | 175 +++++++++++++++++- .../test-chain-v2-block-timestamps.json | 12 +- .../test-chain-v2-disable-reference.json | 12 +- .../test-chain-v2-founder-rewards.json | 12 +- .../test-chain-v2-leftover-reward.json | 12 +- src/test/resources/test-chain-v2-minting.json | 12 +- .../test-chain-v2-qora-holder-extremes.json | 12 +- .../resources/test-chain-v2-qora-holder.json | 12 +- .../test-chain-v2-reward-levels.json | 12 +- .../test-chain-v2-reward-scaling.json | 12 +- .../test-chain-v2-reward-shares.json | 12 +- src/test/resources/test-chain-v2.json | 12 +- 15 files changed, 348 insertions(+), 70 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index bbc6e31b..8fc2b0b9 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -199,6 +199,11 @@ public class Block { } + public boolean hasShareBin(AccountLevelShareBin shareBin, int blockHeight) { + AccountLevelShareBin ourShareBin = this.getShareBin(blockHeight); + return ourShareBin != null && shareBin.id == ourShareBin.id; + } + public long distribute(long accountAmount, Map balanceChanges) { if (this.isRecipientAlsoMinter) { // minter & recipient the same - simpler case @@ -1891,12 +1896,67 @@ public class Block { final boolean haveFounders = !onlineFounderAccounts.isEmpty(); // Determine reward candidates based on account level - List accountLevelShareBins = BlockChain.getInstance().getAccountLevelShareBins(); - for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) { - // Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out. + // This needs a deep copy, so the shares can be modified when tiers aren't activated yet + List accountLevelShareBins = new ArrayList<>(); + for (AccountLevelShareBin accountLevelShareBin : BlockChain.getInstance().getAccountLevelShareBins()) { + accountLevelShareBins.add((AccountLevelShareBin) accountLevelShareBin.clone()); + } + + Map> accountsForShareBin = new HashMap<>(); + + // We might need to combine some share bins if they haven't reached the minimum number of minters yet + for (int binIndex = accountLevelShareBins.size()-1; binIndex >= 0; --binIndex) { AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex); - // Object reference compare is OK as all references are read-only from blockchain config. - List binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin(this.blockData.getHeight()) == accountLevelShareBin).collect(Collectors.toList()); + + // Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out. + List binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.hasShareBin(accountLevelShareBin, this.blockData.getHeight())).collect(Collectors.toList()); + // Add any accounts that have been moved down from a higher tier + List existingBinnedAccounts = accountsForShareBin.get(binIndex); + if (existingBinnedAccounts != null) + binnedAccounts.addAll(existingBinnedAccounts); + + // Logic below may only apply to higher levels, and only for share bins with a specific range of online accounts + if (accountLevelShareBin.levels.get(0) < BlockChain.getInstance().getShareBinActivationMinLevel() || + binnedAccounts.isEmpty() || binnedAccounts.size() >= BlockChain.getInstance().getMinAccountsToActivateShareBin()) { + // Add all accounts for this share bin to the accountsForShareBin list + accountsForShareBin.put(binIndex, binnedAccounts); + continue; + } + + // Share bin contains more than one, but less than the minimum number of minters. We treat this share bin + // as not activated yet. In these cases, the rewards and minters are combined and paid out to the previous + // share bin, to prevent a single or handful of accounts receiving the entire rewards for a share bin. + // + // Example: + // + // - Share bin for levels 5 and 6 has 100 minters + // - Share bin for levels 7 and 8 has 10 minters + // + // This is below the minimum of 30, so share bins are reconstructed as follows: + // + // - Share bin for levels 5 and 6 now contains 110 minters + // - Share bin for levels 7 and 8 now contains 0 minters + // - Share bin for levels 5 and 6 now pays out rewards for levels 5, 6, 7, and 8 + // - Share bin for levels 7 and 8 pays zero rewards + // + // This process is iterative, so will combine several tiers if needed. + + // Designate this share bin as empty + accountsForShareBin.put(binIndex, new ArrayList<>()); + + // Move the accounts originally intended for this share bin to the previous one + accountsForShareBin.put(binIndex - 1, binnedAccounts); + + // Move the block reward from this share bin to the previous one + AccountLevelShareBin previousShareBin = accountLevelShareBins.get(binIndex - 1); + previousShareBin.share += accountLevelShareBin.share; + accountLevelShareBin.share = 0L; + } + + // Now loop through (potentially modified) share bins and determine the reward candidates + for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) { + AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex); + List binnedAccounts = accountsForShareBin.get(binIndex); // No online accounts in this bin? Skip to next one if (binnedAccounts.isEmpty()) diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 239ebaa2..412f8ede 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -104,10 +104,23 @@ public class BlockChain { private List rewardsByHeight; /** Share of block reward/fees by account level */ - public static class AccountLevelShareBin { + public static class AccountLevelShareBin implements Cloneable { + public int id; public List levels; @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) public long share; + + public Object clone() { + AccountLevelShareBin shareBinCopy = new AccountLevelShareBin(); + List levelsCopy = new ArrayList<>(); + for (Integer level : this.levels) { + levelsCopy.add(level); + } + shareBinCopy.id = this.id; + shareBinCopy.levels = levelsCopy; + shareBinCopy.share = this.share; + return shareBinCopy; + } } private List sharesByLevel; /** Generated lookup of share-bin by account level */ @@ -125,6 +138,12 @@ public class BlockChain { @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) private Long qoraPerQortReward; + /** Minimum number of accounts before a share bin is considered activated */ + private int minAccountsToActivateShareBin; + + /** Min level at which share bin activation takes place; lower levels allow less than minAccountsPerShareBin */ + private int shareBinActivationMinLevel; + /** * Number of minted blocks required to reach next level from previous. *

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