sharesByLevel;
/** Generated lookup of share-bin by account level */
@@ -121,6 +134,12 @@ public class BlockChain {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraPerQortReward;
+ /** Minimum number of accounts before a share bin is considered activated */
+ private int minAccountsToActivateShareBin;
+
+ /** Min level at which share bin activation takes place; lower levels allow less than minAccountsPerShareBin */
+ private int shareBinActivationMinLevel;
+
/**
* Number of minted blocks required to reach next level from previous.
*
@@ -166,6 +185,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;
+
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@@ -321,6 +344,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;
@@ -362,6 +390,14 @@ public class BlockChain {
return this.qoraPerQortReward;
}
+ public int getMinAccountsToActivateShareBin() {
+ return this.minAccountsToActivateShareBin;
+ }
+
+ public int getShareBinActivationMinLevel() {
+ return this.shareBinActivationMinLevel;
+ }
+
public int getMinAccountLevelToMint() {
return this.minAccountLevelToMint;
}
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index 2d736e76..343ab4af 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -90,37 +90,40 @@ public class BlockMinter extends Thread {
List newBlocks = new ArrayList<>();
- // Flags for tracking change in whether minting is possible,
- // so we can notify Controller, and further update SysTray, etc.
- boolean isMintingPossible = false;
- boolean wasMintingPossible = isMintingPossible;
- while (running) {
- if (isMintingPossible != wasMintingPossible)
- Controller.getInstance().onMintingPossibleChange(isMintingPossible);
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Going to need this a lot...
+ BlockRepository blockRepository = repository.getBlockRepository();
- wasMintingPossible = isMintingPossible;
+ // Flags for tracking change in whether minting is possible,
+ // so we can notify Controller, and further update SysTray, etc.
+ boolean isMintingPossible = false;
+ boolean wasMintingPossible = isMintingPossible;
+ while (running) {
+ if (isMintingPossible != wasMintingPossible)
+ Controller.getInstance().onMintingPossibleChange(isMintingPossible);
- try {
- // Sleep for a while
- Thread.sleep(1000);
+ wasMintingPossible = isMintingPossible;
- isMintingPossible = false;
+ try {
+ // Free up any repository locks
+ repository.discardChanges();
- final Long now = NTP.getTime();
- if (now == null)
- continue;
+ // Sleep for a while
+ Thread.sleep(1000);
- final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
- if (minLatestBlockTimestamp == null)
- continue;
+ isMintingPossible = false;
- // No online accounts for current timestamp? (e.g. during startup)
- if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
- continue;
+ final Long now = NTP.getTime();
+ if (now == null)
+ continue;
- try (final Repository repository = RepositoryManager.getRepository()) {
- // Going to need this a lot...
- BlockRepository blockRepository = repository.getBlockRepository();
+ final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
+ if (minLatestBlockTimestamp == null)
+ continue;
+
+ // No online accounts for current timestamp? (e.g. during startup)
+ if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
+ continue;
List mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
// No minting accounts?
@@ -198,10 +201,6 @@ public class BlockMinter extends Thread {
// so go ahead and mint a block if possible.
isMintingPossible = true;
- // Reattach newBlocks to new repository handle
- for (Block newBlock : newBlocks)
- newBlock.setRepository(repository);
-
// Check blockchain hasn't changed
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
previousBlockData = lastBlockData;
@@ -439,13 +438,13 @@ public class BlockMinter extends Thread {
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
}
- } catch (DataException e) {
- LOGGER.warn("Repository issue while running block minter", e);
+ } catch (InterruptedException e) {
+ // We've been interrupted - time to exit
+ return;
}
- } catch (InterruptedException e) {
- // We've been interrupted - time to exit
- return;
}
+ } catch (DataException e) {
+ LOGGER.warn("Repository issue while running block minter - NO LONGER MINTING", e);
}
}
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
index 9a82df00..839620a0 100644
--- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java
+++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
@@ -36,7 +36,8 @@ public class OnlineAccountsManager {
/**
* How long online accounts signatures last before they expire.
*/
- public static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
+ private static final long ONLINE_TIMESTAMP_MODULUS_V1 = 5 * 60 * 1000L;
+ private static final long ONLINE_TIMESTAMP_MODULUS_V2 = 30 * 60 * 1000L;
/**
* How many 'current' timestamp-sets of online accounts we cache.
@@ -78,12 +79,20 @@ public class OnlineAccountsManager {
private boolean hasOurOnlineAccounts = false;
+ public static long getOnlineTimestampModulus() {
+ Long now = NTP.getTime();
+ if (now != null && now >= BlockChain.getInstance().getOnlineAccountsModulusV2Timestamp()) {
+ return ONLINE_TIMESTAMP_MODULUS_V2;
+ }
+ return ONLINE_TIMESTAMP_MODULUS_V1;
+ }
public static Long getCurrentOnlineAccountTimestamp() {
Long now = NTP.getTime();
if (now == null)
return null;
- return (now / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS;
+ long onlineTimestampModulus = getOnlineTimestampModulus();
+ return (now / onlineTimestampModulus) * onlineTimestampModulus;
}
private OnlineAccountsManager() {
@@ -203,11 +212,17 @@ public class OnlineAccountsManager {
long onlineAccountTimestamp = onlineAccountData.getTimestamp();
// Check timestamp is 'recent' here
- if (Math.abs(onlineAccountTimestamp - now) > ONLINE_TIMESTAMP_MODULUS * 2) {
+ if (Math.abs(onlineAccountTimestamp - now) > getOnlineTimestampModulus() * 2) {
LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
return false;
}
+ // 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;
+ }
+
// Verify signature
byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
boolean isSignatureValid = onlineAccountTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()
@@ -308,7 +323,7 @@ public class OnlineAccountsManager {
if (now == null)
return;
- final long cutoffThreshold = now - MAX_CACHED_TIMESTAMP_SETS * ONLINE_TIMESTAMP_MODULUS;
+ final long cutoffThreshold = now - MAX_CACHED_TIMESTAMP_SETS * getOnlineTimestampModulus();
this.currentOnlineAccounts.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
this.currentOnlineAccountsHashes.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
}
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
index f390d962..53388418 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;
@@ -61,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;
@@ -412,9 +408,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 {
@@ -441,12 +434,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;
@@ -633,7 +626,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 1af88cb6..20195050 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -289,6 +289,16 @@ public class Settings {
private Long testNtpOffset = null;
+ /* Foreign chains */
+
+ /** 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)
/** Data storage enabled/disabled*/
@@ -885,6 +895,15 @@ public class Settings {
}
+ public int getGapLimit() {
+ return this.gapLimit;
+ }
+
+ public int getBitcoinjLookaheadSize() {
+ return bitcoinjLookaheadSize;
+ }
+
+
public boolean isQdnEnabled() {
return this.qdnEnabled;
}
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 b65fd72e..670027d5 100644
--- a/src/main/resources/blockchain.json
+++ b/src/main/resources/blockchain.json
@@ -23,6 +23,7 @@
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 43200000,
"onlineAccountSignaturesMaxLifetime": 86400000,
+ "onlineAccountsModulusV2Timestamp": 9999999999999,
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },
@@ -39,14 +40,16 @@
{ "height": 3110401, "reward": 2.00 }
],
"sharesByLevel": [
- { "levels": [ 1, 2 ], "share": 0.05 },
- { "levels": [ 3, 4 ], "share": 0.10 },
- { "levels": [ 5, 6 ], "share": 0.15 },
- { "levels": [ 7, 8 ], "share": 0.20 },
- { "levels": [ 9, 10 ], "share": 0.25 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java
index f7970ace..4aee2de1 100644
--- a/src/test/java/org/qortal/test/minting/RewardTests.java
+++ b/src/test/java/org/qortal/test/minting/RewardTests.java
@@ -3,10 +3,9 @@ package org.qortal.test.minting;
import static org.junit.Assert.*;
import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.After;
@@ -702,6 +701,15 @@ public class RewardTests extends Common {
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel7And8Reward);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel7And8Reward);
+ // Orphan and ensure balances return to their previous values
+ BlockUtils.orphanBlocks(repository, 1);
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance);
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance);
+
}
}
@@ -787,6 +795,348 @@ public class RewardTests extends Common {
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel9And10Reward);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel9And10Reward);
+ // Orphan and ensure balances return to their previous values
+ BlockUtils.orphanBlocks(repository, 1);
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance);
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance);
+
+ }
+ }
+
+ /** Test rewards for level 7 and 8 accounts, when the tier doesn't yet have enough minters in it */
+ @Test
+ public void testLevel7And8RewardsPreActivation() throws DataException, IllegalAccessException {
+ Common.useSettings("test-settings-v2-reward-levels.json");
+
+ // Set minAccountsToActivateShareBin to 3 so that share bins 7-8 and 9-10 are considered inactive
+ FieldUtils.writeField(BlockChain.getInstance(), "minAccountsToActivateShareBin", 3, true);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
+ List mintingAndOnlineAccounts = new ArrayList<>();
+
+ // Alice self share online
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ mintingAndOnlineAccounts.add(aliceSelfShare);
+
+ // Bob self-share NOT online
+
+ // Chloe self share online
+ byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
+ PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
+ mintingAndOnlineAccounts.add(chloeRewardShareAccount);
+
+ // Dilbert self share online
+ byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
+ PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
+ mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
+
+ // Mint enough blocks to bump testAccount levels to 7 and 8
+ final int minterBlocksNeeded = cumulativeBlocksByLevel.get(8) - 20; // 20 blocks before level 8, so that the test accounts reach the correct levels
+ for (int bc = 0; bc < minterBlocksNeeded; ++bc)
+ BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that the levels are as we expect
+ assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel());
+ assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
+ assertEquals(7, (int) Common.getTestAccount(repository, "chloe").getLevel());
+ assertEquals(8, (int) Common.getTestAccount(repository, "dilbert").getLevel());
+
+ // Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances
+ Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
+ final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
+ final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
+ final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
+ final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
+
+ // Mint a block
+ final long blockReward = BlockUtils.getNextBlockReward(repository);
+ BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure we are using the correct block reward value
+ assertEquals(100000000L, blockReward);
+
+ /*
+ * Alice, Chloe, and Dilbert are 'online'.
+ * Chloe is level 7; Dilbert is level 8.
+ * One founder online (Alice, who is also level 7).
+ * No legacy QORA holders.
+ *
+ * Level 7 and 8 is not yet activated, so its rewards are added to the level 5 and 6 share bin.
+ * There are no level 5 and 6 online.
+ * Chloe and Dilbert should receive equal shares of the 35% block reward for levels 5 to 8.
+ * Alice should receive the remainder (65%).
+ */
+
+ final int level5To8SharePercent = 35_00; // 35% (combined 15% and 20%)
+ final long level5To8ShareAmount = (blockReward * level5To8SharePercent) / 100L / 100L;
+ final long expectedLevel5To8Reward = level5To8ShareAmount / 2; // The reward is split between Chloe and Dilbert
+ final long expectedFounderReward = blockReward - level5To8ShareAmount; // Alice should receive the remainder
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To8Reward);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To8Reward);
+
+ // Orphan and ensure balances return to their previous values
+ BlockUtils.orphanBlocks(repository, 1);
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance);
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance);
+
+ }
+ }
+
+ /** Test rewards for level 9 and 10 accounts, when the tier doesn't yet have enough minters in it.
+ * Tier 7-8 isn't activated either, so the rewards and minters are all moved to tier 5-6. */
+ @Test
+ public void testLevel9And10RewardsPreActivation() throws DataException, IllegalAccessException {
+ Common.useSettings("test-settings-v2-reward-levels.json");
+
+ // Set minAccountsToActivateShareBin to 3 so that share bins 7-8 and 9-10 are considered inactive
+ FieldUtils.writeField(BlockChain.getInstance(), "minAccountsToActivateShareBin", 3, true);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
+ List mintingAndOnlineAccounts = new ArrayList<>();
+
+ // Alice self share online
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ mintingAndOnlineAccounts.add(aliceSelfShare);
+
+ // Bob self-share not initially online
+
+ // Chloe self share online
+ byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
+ PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
+ mintingAndOnlineAccounts.add(chloeRewardShareAccount);
+
+ // Dilbert self share online
+ byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
+ PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
+ mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
+
+ // Mint enough blocks to bump testAccount levels to 9 and 10
+ final int minterBlocksNeeded = cumulativeBlocksByLevel.get(10) - 20; // 20 blocks before level 10, so that the test accounts reach the correct levels
+ for (int bc = 0; bc < minterBlocksNeeded; ++bc)
+ BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Bob self-share now comes online
+ byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0);
+ PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey);
+ mintingAndOnlineAccounts.add(bobRewardShareAccount);
+
+ // Ensure that the levels are as we expect
+ assertEquals(9, (int) Common.getTestAccount(repository, "alice").getLevel());
+ assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
+ assertEquals(9, (int) Common.getTestAccount(repository, "chloe").getLevel());
+ assertEquals(10, (int) Common.getTestAccount(repository, "dilbert").getLevel());
+
+ // Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances
+ Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
+ final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
+ final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
+ final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
+ final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
+
+ // Mint a block
+ final long blockReward = BlockUtils.getNextBlockReward(repository);
+ BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure we are using the correct block reward value
+ assertEquals(100000000L, blockReward);
+
+ /*
+ * Alice, Bob, Chloe, and Dilbert are 'online'.
+ * Bob is level 1; Chloe is level 9; Dilbert is level 10.
+ * One founder online (Alice, who is also level 9).
+ * No legacy QORA holders.
+ *
+ * Levels 7+8, and 9+10 are not yet activated, so their rewards are added to the level 5 and 6 share bin.
+ * There are no levels 5-8 online.
+ * Chloe and Dilbert should receive equal shares of the 60% block reward for levels 5 to 10.
+ * Alice should receive the remainder (40%).
+ */
+
+ final int level1And2SharePercent = 5_00; // 5%
+ final int level5To10SharePercent = 60_00; // 60% (combined 15%, 20%, and 25%)
+ final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L;
+ final long level5To10ShareAmount = (blockReward * level5To10SharePercent) / 100L / 100L;
+ final long expectedLevel1And2Reward = level1And2ShareAmount; // The reward is given entirely to Bob
+ final long expectedLevel5To10Reward = level5To10ShareAmount / 2; // The reward is split between Chloe and Dilbert
+ final long expectedFounderReward = blockReward - level1And2ShareAmount - level5To10ShareAmount; // Alice should receive the remainder
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedLevel1And2Reward);
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To10Reward);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To10Reward);
+
+ // Orphan and ensure balances return to their previous values
+ BlockUtils.orphanBlocks(repository, 1);
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance);
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance);
+
+ }
+ }
+
+ /** Test rewards for level 7 and 8 accounts, when the tier reaches the minimum number of accounts */
+ @Test
+ public void testLevel7And8RewardsPreAndPostActivation() throws DataException, IllegalAccessException {
+ Common.useSettings("test-settings-v2-reward-levels.json");
+
+ // Set minAccountsToActivateShareBin to 2 so that share bins 7-8 and 9-10 are considered inactive at first
+ FieldUtils.writeField(BlockChain.getInstance(), "minAccountsToActivateShareBin", 2, true);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
+ List mintingAndOnlineAccounts = new ArrayList<>();
+
+ // Alice self share online
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ mintingAndOnlineAccounts.add(aliceSelfShare);
+
+ // Bob self-share NOT online
+
+ // Chloe self share online
+ byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
+ PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
+ mintingAndOnlineAccounts.add(chloeRewardShareAccount);
+
+ // Dilbert self share online
+ byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
+ PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
+ mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
+
+ // Mint enough blocks to bump two of the testAccount levels to 7
+ final int minterBlocksNeeded = cumulativeBlocksByLevel.get(7) - 12; // 12 blocks before level 7, so that dilbert and alice have reached level 7, but chloe will reach it in the next 2 blocks
+ for (int bc = 0; bc < minterBlocksNeeded; ++bc)
+ BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that the levels are as we expect
+ assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel());
+ assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
+ assertEquals(6, (int) Common.getTestAccount(repository, "chloe").getLevel());
+ assertEquals(7, (int) Common.getTestAccount(repository, "dilbert").getLevel());
+
+ // Now that dilbert has reached level 7, we can capture initial balances
+ Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
+ final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
+ final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
+ final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
+ final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
+
+ // Mint a block
+ long blockReward = BlockUtils.getNextBlockReward(repository);
+ BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure we are using the correct block reward value
+ assertEquals(100000000L, blockReward);
+
+ /*
+ * Alice, Chloe, and Dilbert are 'online'.
+ * Chloe is level 6; Dilbert is level 7.
+ * One founder online (Alice, who is also level 7).
+ * No legacy QORA holders.
+ *
+ * Level 7 and 8 is not yet activated, so its rewards are added to the level 5 and 6 share bin.
+ * There are no level 5 and 6 online.
+ * Chloe and Dilbert should receive equal shares of the 35% block reward for levels 5 to 8.
+ * Alice should receive the remainder (65%).
+ */
+
+ final int level5To8SharePercent = 35_00; // 35% (combined 15% and 20%)
+ final long level5To8ShareAmount = (blockReward * level5To8SharePercent) / 100L / 100L;
+ final long expectedLevel5To8Reward = level5To8ShareAmount / 2; // The reward is split between Chloe and Dilbert
+ final long expectedFounderReward = blockReward - level5To8ShareAmount; // Alice should receive the remainder
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5To8Reward);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5To8Reward);
+
+ // Ensure that the levels are as we expect
+ assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel());
+ assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
+ assertEquals(6, (int) Common.getTestAccount(repository, "chloe").getLevel());
+ assertEquals(7, (int) Common.getTestAccount(repository, "dilbert").getLevel());
+
+ // Capture pre-activation balances
+ Map> preActivationBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
+ final long alicePreActivationBalance = preActivationBalances.get("alice").get(Asset.QORT);
+ final long bobPreActivationBalance = preActivationBalances.get("bob").get(Asset.QORT);
+ final long chloePreActivationBalance = preActivationBalances.get("chloe").get(Asset.QORT);
+ final long dilbertPreActivationBalance = preActivationBalances.get("dilbert").get(Asset.QORT);
+
+ // Mint another block
+ blockReward = BlockUtils.getNextBlockReward(repository);
+ BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that the levels are as we expect (chloe has now increased to level 7; level 7-8 is now activated)
+ assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel());
+ assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
+ assertEquals(7, (int) Common.getTestAccount(repository, "chloe").getLevel());
+ assertEquals(7, (int) Common.getTestAccount(repository, "dilbert").getLevel());
+
+ /*
+ * Alice, Chloe, and Dilbert are 'online'.
+ * Chloe and Dilbert are level 7.
+ * One founder online (Alice, who is also level 7).
+ * No legacy QORA holders.
+ *
+ * Level 7 and 8 is now activated, so its rewards are paid out in the normal way.
+ * There are no level 5 and 6 online.
+ * Chloe and Dilbert should receive equal shares of the 20% block reward for levels 7 to 8.
+ * Alice should receive the remainder (80%).
+ */
+
+ final int level7To8SharePercent = 20_00; // 20%
+ final long level7To8ShareAmount = (blockReward * level7To8SharePercent) / 100L / 100L;
+ final long expectedLevel7To8Reward = level7To8ShareAmount / 2; // The reward is split between Chloe and Dilbert
+ final long newExpectedFounderReward = blockReward - level7To8ShareAmount; // Alice should receive the remainder
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, alicePreActivationBalance+newExpectedFounderReward);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobPreActivationBalance); // Bob not online so his balance remains the same
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloePreActivationBalance+expectedLevel7To8Reward);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertPreActivationBalance+expectedLevel7To8Reward);
+
+
+ // Orphan and ensure balances return to their pre-activation values
+ BlockUtils.orphanBlocks(repository, 1);
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, alicePreActivationBalance);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobPreActivationBalance);
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloePreActivationBalance);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertPreActivationBalance);
+
+
+ // Orphan again and ensure balances return to their initial values
+ BlockUtils.orphanBlocks(repository, 1);
+
+ // Validate the balances
+ AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance);
+ AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance);
+ AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance);
+ AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance);
+
}
}
diff --git a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java
index 4154121c..0b554b6a 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("test-settings-v2-no-sig-agg.json", 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-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json
index 38a18a8c..d7beba3c 100644
--- a/src/test/resources/test-chain-v2-block-timestamps.json
+++ b/src/test/resources/test-chain-v2-block-timestamps.json
@@ -20,14 +20,16 @@
{ "height": 21, "reward": 1 }
],
"sharesByLevel": [
- { "levels": [ 1, 2 ], "share": 0.05 },
- { "levels": [ 3, 4 ], "share": 0.10 },
- { "levels": [ 5, 6 ], "share": 0.15 },
- { "levels": [ 7, 8 ], "share": 0.20 },
- { "levels": [ 9, 10 ], "share": 0.25 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 },
diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json
index 648e91b5..3ad7da60 100644
--- a/src/test/resources/test-chain-v2-disable-reference.json
+++ b/src/test/resources/test-chain-v2-disable-reference.json
@@ -24,14 +24,16 @@
{ "height": 21, "reward": 1 }
],
"sharesByLevel": [
- { "levels": [ 1, 2 ], "share": 0.05 },
- { "levels": [ 3, 4 ], "share": 0.10 },
- { "levels": [ 5, 6 ], "share": 0.15 },
- { "levels": [ 7, 8 ], "share": 0.20 },
- { "levels": [ 9, 10 ], "share": 0.25 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json
index 540d7efd..c9d63800 100644
--- a/src/test/resources/test-chain-v2-founder-rewards.json
+++ b/src/test/resources/test-chain-v2-founder-rewards.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json
index ffd81379..ad5a1f9a 100644
--- a/src/test/resources/test-chain-v2-leftover-reward.json
+++ b/src/test/resources/test-chain-v2-leftover-reward.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json
index 8d66e072..1d57e119 100644
--- a/src/test/resources/test-chain-v2-minting.json
+++ b/src/test/resources/test-chain-v2-minting.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-no-sig-agg.json b/src/test/resources/test-chain-v2-no-sig-agg.json
new file mode 100644
index 00000000..71e1cc3d
--- /dev/null
+++ b/src/test/resources/test-chain-v2-no-sig-agg.json
@@ -0,0 +1,87 @@
+{
+ "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,
+ "rewardShareLimitTimestamp": 9999999999999,
+ "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-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json
index 9e8ff2a8..97d7a320 100644
--- a/src/test/resources/test-chain-v2-qora-holder-extremes.json
+++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json
index 0dac2457..6d0c2223 100644
--- a/src/test/resources/test-chain-v2-qora-holder.json
+++ b/src/test/resources/test-chain-v2-qora-holder.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json
index 90d201a3..03a05a5e 100644
--- a/src/test/resources/test-chain-v2-reward-levels.json
+++ b/src/test/resources/test-chain-v2-reward-levels.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 1,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json
index 6b2cbc0c..a108c651 100644
--- a/src/test/resources/test-chain-v2-reward-scaling.json
+++ b/src/test/resources/test-chain-v2-reward-scaling.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json
index 9e713095..8f14e48f 100644
--- a/src/test/resources/test-chain-v2-reward-shares.json
+++ b/src/test/resources/test-chain-v2-reward-shares.json
@@ -24,14 +24,16 @@
{ "height": 21, "reward": 1 }
],
"sharesByLevel": [
- { "levels": [ 1, 2 ], "share": 0.05 },
- { "levels": [ 3, 4 ], "share": 0.10 },
- { "levels": [ 5, 6 ], "share": 0.15 },
- { "levels": [ 7, 8 ], "share": 0.20 },
- { "levels": [ 9, 10 ], "share": 0.25 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json
index c08dac04..3b380d29 100644
--- a/src/test/resources/test-chain-v2.json
+++ b/src/test/resources/test-chain-v2.json
@@ -18,20 +18,23 @@
"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 }
+ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
+ { "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
+ { "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
+ { "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
+ { "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
+ "minAccountsToActivateShareBin": 30,
+ "shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
diff --git a/src/test/resources/test-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
+}