From f7dabcaeb00f377396a14f7a790db85999213597 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 1 Apr 2022 11:35:32 +0100 Subject: [PATCH 01/16] Increase ONLINE_ACCOUNTS_MODULUS from 5 to 30 mins at a future undecided timestamp. Note: it's important that this timestamp is set on a 1-hour boundary (such as 16:00:00) to ensure a clean switchover. # Conflicts: # src/main/java/org/qortal/block/BlockChain.java --- src/main/java/org/qortal/block/Block.java | 2 +- .../java/org/qortal/block/BlockChain.java | 9 ++ .../controller/OnlineAccountsManager.java | 18 +++- .../transaction/PresenceTransaction.java | 3 +- src/main/resources/blockchain.json | 1 + .../test/network/OnlineAccountsTests.java | 97 ++++++++++++++++++- .../test-chain-v2-founder-rewards.json | 1 + .../test-chain-v2-leftover-reward.json | 1 + src/test/resources/test-chain-v2-minting.json | 1 + .../test-chain-v2-qora-holder-extremes.json | 1 + .../resources/test-chain-v2-qora-holder.json | 1 + .../test-chain-v2-reward-levels.json | 1 + .../test-chain-v2-reward-scaling.json | 1 + src/test/resources/test-chain-v2.json | 1 + 14 files changed, 127 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index ea5a6b49..7800f2a1 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -987,7 +987,7 @@ public class Block { byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp); // If this block is much older than current online timestamp, then there's no point checking current online accounts - List currentOnlineAccounts = onlineTimestamp < NTP.getTime() - OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS + List currentOnlineAccounts = onlineTimestamp < NTP.getTime() - OnlineAccountsManager.getOnlineTimestampModulus() ? null : OnlineAccountsManager.getInstance().getOnlineAccounts(); List latestBlocksOnlineAccounts = OnlineAccountsManager.getInstance().getLatestBlocksOnlineAccounts(); diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index bc06fadf..135708ab 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -162,6 +162,10 @@ public class BlockChain { /** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */ private long onlineAccountSignaturesMaxLifetime; + /** Feature trigger timestamp for ONLINE_ACCOUNTS_MODULUS time interval increase. Can't use + * featureTriggers because unit tests need to set this value via Reflection. */ + private long onlineAccountsModulusV2Timestamp; + /** Settings relating to CIYAM AT feature. */ public static class CiyamAtSettings { /** Fee per step/op-code executed. */ @@ -310,6 +314,11 @@ public class BlockChain { return this.maxBlockSize; } + // Online accounts + public long getOnlineAccountsModulusV2Timestamp() { + return this.onlineAccountsModulusV2Timestamp; + } + /** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */ public boolean getRequireGroupForApproval() { return this.requireGroupForApproval; diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java index 70b04e56..a5f86b48 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -50,8 +50,8 @@ public class OnlineAccountsManager extends Thread { // To do with online accounts list private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 1 * 60 * 1000L; // ms - public static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L; - private static final long LAST_SEEN_EXPIRY_PERIOD = (ONLINE_TIMESTAMP_MODULUS * 2) + (1 * 60 * 1000L); + public static final long ONLINE_TIMESTAMP_MODULUS_V1 = 5 * 60 * 1000L; + public static final long ONLINE_TIMESTAMP_MODULUS_V2 = 30 * 60 * 1000L; /** How many (latest) blocks' worth of online accounts we cache */ private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 2; private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L; @@ -116,6 +116,13 @@ public class OnlineAccountsManager extends Thread { this.interrupt(); } + public static long getOnlineTimestampModulus() { + if (NTP.getTime() >= BlockChain.getInstance().getOnlineAccountsModulusV2Timestamp()) { + return ONLINE_TIMESTAMP_MODULUS_V2; + } + return ONLINE_TIMESTAMP_MODULUS_V1; + } + // Online accounts import queue @@ -159,7 +166,7 @@ public class OnlineAccountsManager extends Thread { PublicKeyAccount otherAccount = new PublicKeyAccount(repository, onlineAccountData.getPublicKey()); // Check timestamp is 'recent' here - if (Math.abs(onlineAccountData.getTimestamp() - now) > ONLINE_TIMESTAMP_MODULUS * 2) { + if (Math.abs(onlineAccountData.getTimestamp() - now) > getOnlineTimestampModulus() * 2) { LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp())); return; } @@ -241,7 +248,8 @@ public class OnlineAccountsManager extends Thread { return; // Expire old entries - final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD; + final long lastSeenExpiryPeriod = (getOnlineTimestampModulus() * 2) + (1 * 60 * 1000L); + final long cutoffThreshold = now - lastSeenExpiryPeriod; synchronized (this.onlineAccounts) { Iterator iterator = this.onlineAccounts.iterator(); while (iterator.hasNext()) { @@ -372,7 +380,7 @@ public class OnlineAccountsManager extends Thread { } public static long toOnlineAccountTimestamp(long timestamp) { - return (timestamp / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS; + return (timestamp / getOnlineTimestampModulus()) * getOnlineTimestampModulus(); } /** Returns list of online accounts with timestamp recent enough to be considered currently online. */ diff --git a/src/main/java/org/qortal/transaction/PresenceTransaction.java b/src/main/java/org/qortal/transaction/PresenceTransaction.java index d0f54548..8076997c 100644 --- a/src/main/java/org/qortal/transaction/PresenceTransaction.java +++ b/src/main/java/org/qortal/transaction/PresenceTransaction.java @@ -12,7 +12,6 @@ import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.account.Account; -import org.qortal.controller.Controller; import org.qortal.controller.OnlineAccountsManager; import org.qortal.controller.tradebot.TradeBot; import org.qortal.crosschain.ACCT; @@ -49,7 +48,7 @@ public class PresenceTransaction extends Transaction { REWARD_SHARE(0) { @Override public long getLifetime() { - return OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS; + return OnlineAccountsManager.getOnlineTimestampModulus(); } }, TRADE_BOT(1) { diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 1f20ccfe..5a59df91 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -19,6 +19,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 43200000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 5.00 }, { "height": 259201, "reward": 4.75 }, diff --git a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java index 4154121c..c804c7db 100644 --- a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java +++ b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java @@ -1,22 +1,36 @@ package org.qortal.test.network; +import org.apache.commons.lang3.reflect.FieldUtils; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.junit.Before; import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.block.Block; +import org.qortal.block.BlockChain; +import org.qortal.controller.BlockMinter; +import org.qortal.controller.OnlineAccountsManager; import org.qortal.data.network.OnlineAccountData; import org.qortal.network.message.*; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.test.common.Common; import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; +import org.qortal.utils.NTP; +import java.io.IOException; import java.nio.ByteBuffer; import java.security.Security; import java.util.ArrayList; import java.util.List; import java.util.Random; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; -public class OnlineAccountsTests { +public class OnlineAccountsTests extends Common { private static final Random RANDOM = new Random(); static { @@ -27,6 +41,12 @@ public class OnlineAccountsTests { Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); } + @Before + public void beforeTest() throws DataException, IOException { + Common.useSettingsAndDb(Common.testSettingsFilename, false); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + } + @Test public void testGetOnlineAccountsV2() throws MessageException { @@ -111,4 +131,75 @@ public class OnlineAccountsTests { return onlineAccounts; } + @Test + public void testOnlineAccountsModulusV1() throws IllegalAccessException, DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Set feature trigger timestamp to MAX long so that it is inactive + FieldUtils.writeField(BlockChain.getInstance(), "onlineAccountsModulusV2Timestamp", Long.MAX_VALUE, true); + + List onlineAccountSignatures = new ArrayList<>(); + long fakeNTPOffset = 0L; + + // Mint a block and store its timestamp + Block block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + long lastBlockTimestamp = block.getBlockData().getTimestamp(); + + // Mint some blocks and keep track of the different online account signatures + for (int i = 0; i < 30; i++) { + block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + + // Increase NTP fixed offset by the block time, to simulate time passing + long blockTimeDelta = block.getBlockData().getTimestamp() - lastBlockTimestamp; + lastBlockTimestamp = block.getBlockData().getTimestamp(); + fakeNTPOffset += blockTimeDelta; + NTP.setFixedOffset(fakeNTPOffset); + + String lastOnlineAccountSignatures58 = Base58.encode(block.getBlockData().getOnlineAccountsSignatures()); + if (!onlineAccountSignatures.contains(lastOnlineAccountSignatures58)) { + onlineAccountSignatures.add(lastOnlineAccountSignatures58); + } + } + + // We expect at least 6 unique signatures over 30 blocks (generally 6-8, but could be higher due to block time differences) + System.out.println(String.format("onlineAccountSignatures count: %d", onlineAccountSignatures.size())); + assertTrue(onlineAccountSignatures.size() >= 6); + } + } + + @Test + public void testOnlineAccountsModulusV2() throws IllegalAccessException, DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Set feature trigger timestamp to 0 so that it is active + FieldUtils.writeField(BlockChain.getInstance(), "onlineAccountsModulusV2Timestamp", 0L, true); + + List onlineAccountSignatures = new ArrayList<>(); + long fakeNTPOffset = 0L; + + // Mint a block and store its timestamp + Block block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + long lastBlockTimestamp = block.getBlockData().getTimestamp(); + + // Mint some blocks and keep track of the different online account signatures + for (int i = 0; i < 30; i++) { + block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + + // Increase NTP fixed offset by the block time, to simulate time passing + long blockTimeDelta = block.getBlockData().getTimestamp() - lastBlockTimestamp; + lastBlockTimestamp = block.getBlockData().getTimestamp(); + fakeNTPOffset += blockTimeDelta; + NTP.setFixedOffset(fakeNTPOffset); + + String lastOnlineAccountSignatures58 = Base58.encode(block.getBlockData().getOnlineAccountsSignatures()); + if (!onlineAccountSignatures.contains(lastOnlineAccountSignatures58)) { + onlineAccountSignatures.add(lastOnlineAccountSignatures58); + } + } + + // We expect 1-3 unique signatures over 30 blocks + System.out.println(String.format("onlineAccountSignatures count: %d", onlineAccountSignatures.size())); + assertTrue(onlineAccountSignatures.size() >= 1 && onlineAccountSignatures.size() <= 3); + } + } } diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index c0ea8fe5..b2e4018f 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 01505af0..4fac00b5 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index fcabe4bf..1b85a948 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, 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 8ec94631..daee01a0 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 38a563b2..9146d715 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index ab934d26..dced66c3 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index b3e358b2..1e0f7c6c 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 20ff391c..bf534ce4 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -14,6 +14,7 @@ "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, From ef51cf5702a639080200dbfca2f61844fbbf9e86 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Thu, 2 Jun 2022 12:46:11 +0100 Subject: [PATCH 02/16] Added defensiveness in getOnlineTimestampModulus(), just in case NTP.getTime() returns null --- src/main/java/org/qortal/controller/OnlineAccountsManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java index a5f86b48..092cae05 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -117,7 +117,8 @@ public class OnlineAccountsManager extends Thread { } public static long getOnlineTimestampModulus() { - if (NTP.getTime() >= BlockChain.getInstance().getOnlineAccountsModulusV2Timestamp()) { + Long now = NTP.getTime(); + if (now != null && now >= BlockChain.getInstance().getOnlineAccountsModulusV2Timestamp()) { return ONLINE_TIMESTAMP_MODULUS_V2; } return ONLINE_TIMESTAMP_MODULUS_V1; From 80188629dfa9f466195bd642989b799e017fe25a Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 1 Jul 2022 13:13:22 +0100 Subject: [PATCH 03/16] Don't aggregate signatures when running OnlineAccountsTests, as it's too difficult to check how many unique signatures exist over a given period of time. --- .../test/network/OnlineAccountsTests.java | 2 +- .../resources/test-chain-v2-no-sig-agg.json | 86 +++++++++++++++++++ .../test-settings-v2-no-sig-agg.json | 19 ++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/test-chain-v2-no-sig-agg.json create mode 100644 src/test/resources/test-settings-v2-no-sig-agg.json diff --git a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java index c804c7db..0b554b6a 100644 --- a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java +++ b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java @@ -43,7 +43,7 @@ public class OnlineAccountsTests extends Common { @Before public void beforeTest() throws DataException, IOException { - Common.useSettingsAndDb(Common.testSettingsFilename, false); + Common.useSettingsAndDb("test-settings-v2-no-sig-agg.json", false); NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); } diff --git a/src/test/resources/test-chain-v2-no-sig-agg.json b/src/test/resources/test-chain-v2-no-sig-agg.json new file mode 100644 index 00000000..75c5528c --- /dev/null +++ b/src/test/resources/test-chain-v2-no-sig-agg.json @@ -0,0 +1,86 @@ +{ + "isTestChain": true, + "blockTimestampMargin": 500, + "transactionExpiryPeriod": 86400000, + "maxBlockSize": 2097152, + "maxBytesPerUnitFee": 1024, + "unitFee": "0.1", + "nameRegistrationUnitFees": [ + { "timestamp": 1645372800000, "fee": "5" } + ], + "requireGroupForApproval": false, + "minAccountLevelToRewardShare": 5, + "maxRewardSharesPerMintingAccount": 20, + "founderEffectiveMintingLevel": 10, + "onlineAccountSignaturesMinLifetime": 3600000, + "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, + "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, + "calcChainWeightTimestamp": 0, + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0, + "disableReferenceTimestamp": 9999999999999, + "aggregateSignatureTimestamp": 9999999999999 + }, + "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-settings-v2-no-sig-agg.json b/src/test/resources/test-settings-v2-no-sig-agg.json new file mode 100644 index 00000000..1a55fa65 --- /dev/null +++ b/src/test/resources/test-settings-v2-no-sig-agg.json @@ -0,0 +1,19 @@ +{ + "repositoryPath": "testdb", + "bitcoinNet": "TEST3", + "litecoinNet": "TEST3", + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-no-sig-agg.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 90e8cfc737bf38f8104277a3aa0e1323dfcfdf64 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 8 Jul 2022 11:12:58 +0100 Subject: [PATCH 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 @@ - + From fe9744eec6f43d1edcec6277277e817629420caa Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 30 Jul 2022 14:52:47 +0100 Subject: [PATCH 12/16] Fixed missing feature trigger in testchain config --- src/test/resources/test-chain-v2-no-sig-agg.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/resources/test-chain-v2-no-sig-agg.json b/src/test/resources/test-chain-v2-no-sig-agg.json index 75c5528c..71e1cc3d 100644 --- a/src/test/resources/test-chain-v2-no-sig-agg.json +++ b/src/test/resources/test-chain-v2-no-sig-agg.json @@ -52,6 +52,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, From 55f973af3c405dee893e2d31c7db37dc65dc8c63 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 30 Jul 2022 18:15:16 +0100 Subject: [PATCH 13/16] Ensure all online accounts timestamps are a multiple of the online timestamp modulus. This is a simple way to discard the 5-minute online account timestamps (from out of date nodes) once the switch to 30-minute online account timestamps has taken place. --- .../java/org/qortal/controller/OnlineAccountsManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java index 05fb7f29..6fbeec25 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -217,6 +217,11 @@ public class OnlineAccountsManager { return false; } + // Check timestamp is a multiple of online timestamp modulus + if (onlineAccountTimestamp % getOnlineTimestampModulus() != 0) { + return false; + } + // Verify signature byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp()); boolean isSignatureValid = onlineAccountTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp() From c9966337323a003d7d516b76f4293d89ddb6bd0a Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 30 Jul 2022 19:06:04 +0100 Subject: [PATCH 14/16] Added trace level logging. --- src/main/java/org/qortal/controller/OnlineAccountsManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java index 6fbeec25..839620a0 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -219,6 +219,7 @@ public class OnlineAccountsManager { // Check timestamp is a multiple of online timestamp modulus if (onlineAccountTimestamp % getOnlineTimestampModulus() != 0) { + LOGGER.trace(() -> String.format("Rejecting online account %s with invalid timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp)); return false; } From e71f22fd2cf7d6084e35907fe179717977547eb8 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 31 Jul 2022 11:59:14 +0100 Subject: [PATCH 15/16] Added "gapLimit" setting. This replaces the previously hardcoded "numberOfAdditionalBatchesToSearch" variable, and specifies the minimum number of empty consecutive addresses required before a set of wallet transactions is considered complete. Used for foreign transaction lists and balances. --- src/main/java/org/qortal/crosschain/Bitcoiny.java | 8 +++----- src/main/java/org/qortal/settings/Settings.java | 12 ++++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index 56c5b409..8bfa0646 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -29,6 +29,7 @@ import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.Wallet; import org.qortal.api.model.SimpleForeignTransaction; import org.qortal.crypto.Crypto; +import org.qortal.settings.Settings; import org.qortal.utils.Amounts; import org.qortal.utils.BitTwiddling; @@ -409,9 +410,6 @@ public abstract class Bitcoiny implements ForeignBlockchain { Set walletTransactions = new HashSet<>(); Set keySet = new HashSet<>(); - // Set the number of consecutive empty batches required before giving up - final int numberOfAdditionalBatchesToSearch = 7; - int unusedCounter = 0; int ki = 0; do { @@ -438,12 +436,12 @@ public abstract class Bitcoiny implements ForeignBlockchain { if (areAllKeysUnused) { // No transactions - if (unusedCounter >= numberOfAdditionalBatchesToSearch) { + if (unusedCounter >= Settings.getInstance().getGapLimit()) { // ... and we've hit our search limit break; } // We haven't hit our search limit yet so increment the counter and keep looking - unusedCounter++; + unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT; } else { // Some keys in this batch were used, so reset the counter unusedCounter = 0; diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index e0ed7306..7851e374 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -284,6 +284,13 @@ public class Settings { private Long testNtpOffset = null; + /* Foreign chains */ + + /** Set the number of consecutive empty addresses required before treating a wallet's transaction set as complete */ + private int gapLimit = 24; + + + // Data storage (QDN) /** Data storage enabled/disabled*/ @@ -872,6 +879,11 @@ public class Settings { } + public int getGapLimit() { + return this.gapLimit; + } + + public boolean isQdnEnabled() { return this.qdnEnabled; } From d6d2641cad77595fd4a7cbd97eb87b517322e766 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 31 Jul 2022 12:07:44 +0100 Subject: [PATCH 16/16] Added "bitcoinjLookaheadSize" setting (default 50). This replaces the WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ constant. --- src/main/java/org/qortal/crosschain/Bitcoiny.java | 7 +------ src/main/java/org/qortal/settings/Settings.java | 9 ++++++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index 8bfa0646..6ab7143f 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -62,11 +62,6 @@ public abstract class Bitcoiny implements ForeignBlockchain { /** How many wallet keys to generate in each batch. */ private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3; - /** How many wallet keys to generate when using bitcoinj as the data provider. - * We must use a higher value here since we are unable to request multiple batches of keys. - * Without this, the bitcoinj state can be missing transactions, causing errors such as "insufficient balance". */ - private static final int WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ = 50; - /** Byte offset into raw block headers to block timestamp. */ private static final int TIMESTAMP_OFFSET = 4 + 32 + 32; @@ -628,7 +623,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { this.keyChain = this.wallet.getActiveKeyChain(); // Set up wallet's key chain - this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ); + this.keyChain.setLookaheadSize(Settings.getInstance().getBitcoinjLookaheadSize()); this.keyChain.maybeLookAhead(); } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 7851e374..92730b30 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -286,9 +286,12 @@ public class Settings { /* Foreign chains */ - /** Set the number of consecutive empty addresses required before treating a wallet's transaction set as complete */ + /** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */ private int gapLimit = 24; + /** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */ + private int bitcoinjLookaheadSize = 50; + // Data storage (QDN) @@ -883,6 +886,10 @@ public class Settings { return this.gapLimit; } + public int getBitcoinjLookaheadSize() { + return bitcoinjLookaheadSize; + } + public boolean isQdnEnabled() { return this.qdnEnabled;