diff --git a/pom.xml b/pom.xml
index e951c7c7..9e7c9741 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 3.4.0
+ 3.4.1
jar
true
diff --git a/src/main/java/org/qortal/ApplyUpdate.java b/src/main/java/org/qortal/ApplyUpdate.java
index 796bf580..7a870460 100644
--- a/src/main/java/org/qortal/ApplyUpdate.java
+++ b/src/main/java/org/qortal/ApplyUpdate.java
@@ -8,6 +8,7 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.Security;
import java.util.*;
+import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -18,6 +19,8 @@ import org.qortal.api.ApiRequest;
import org.qortal.controller.AutoUpdate;
import org.qortal.settings.Settings;
+import static org.qortal.controller.AutoUpdate.AGENTLIB_JVM_HOLDER_ARG;
+
public class ApplyUpdate {
static {
@@ -197,6 +200,11 @@ public class ApplyUpdate {
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
+ // Reapply any retained, but disabled, -agentlib JVM arg
+ javaCmd = javaCmd.stream()
+ .map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
+ .collect(Collectors.toList());
+
// Call mainClass in JAR
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
@@ -205,7 +213,7 @@ public class ApplyUpdate {
}
try {
- LOGGER.info(() -> String.format("Restarting node with: %s", String.join(" ", javaCmd)));
+ LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
@@ -214,8 +222,15 @@ public class ApplyUpdate {
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
}
- processBuilder.start();
- } catch (IOException e) {
+ // New process will inherit our stdout and stderr
+ processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+ processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ Process process = processBuilder.start();
+
+ // Nothing to pipe to new process, so close output stream (process's stdin)
+ process.getOutputStream().close();
+ } catch (Exception e) {
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
}
}
diff --git a/src/main/java/org/qortal/api/resource/BlocksResource.java b/src/main/java/org/qortal/api/resource/BlocksResource.java
index 28d54a44..195b2ca4 100644
--- a/src/main/java/org/qortal/api/resource/BlocksResource.java
+++ b/src/main/java/org/qortal/api/resource/BlocksResource.java
@@ -114,7 +114,7 @@ public class BlocksResource {
@Path("/signature/{signature}/data")
@Operation(
summary = "Fetch serialized, base58 encoded block data using base58 signature",
- description = "Returns serialized data for the block that matches the given signature",
+ description = "Returns serialized data for the block that matches the given signature, and an optional block serialization version",
responses = {
@ApiResponse(
description = "the block data",
@@ -125,7 +125,7 @@ public class BlocksResource {
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
})
- public String getSerializedBlockData(@PathParam("signature") String signature58) {
+ public String getSerializedBlockData(@PathParam("signature") String signature58, @QueryParam("version") Integer version) {
// Decode signature
byte[] signature;
try {
@@ -136,20 +136,41 @@ public class BlocksResource {
try (final Repository repository = RepositoryManager.getRepository()) {
+ // Default to version 1
+ if (version == null) {
+ version = 1;
+ }
+
// Check the database first
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData != null) {
Block block = new Block(repository, blockData);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
- bytes.write(BlockTransformer.toBytes(block));
+
+ switch (version) {
+ case 1:
+ bytes.write(BlockTransformer.toBytes(block));
+ break;
+
+ case 2:
+ bytes.write(BlockTransformer.toBytesV2(block));
+ break;
+
+ default:
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+ }
+
return Base58.encode(bytes.toByteArray());
}
// Not found, so try the block archive
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
if (bytes != null) {
- return Base58.encode(bytes);
+ if (version != 1) {
+ throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Archived blocks require version 1");
+ }
+ return Base58.encode(bytes);
}
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index c79d3a30..56294d1a 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -68,6 +68,7 @@ public class BlockChain {
atFindNextTransactionFix,
newBlockSigHeight,
shareBinFix,
+ rewardShareLimitTimestamp,
calcChainWeightTimestamp,
transactionV5Timestamp,
transactionV6Timestamp,
@@ -157,7 +158,7 @@ public class BlockChain {
private int minAccountLevelToMint;
private int minAccountLevelForBlockSubmissions;
private int minAccountLevelToRewardShare;
- private int maxRewardSharesPerMintingAccount;
+ private int maxRewardSharesPerFounderMintingAccount;
private int founderEffectiveMintingLevel;
/** Minimum time to retain online account signatures (ms) for block validity checks. */
@@ -173,6 +174,13 @@ public class BlockChain {
* because unit tests need to set this value via Reflection. */
private long onlineAccountsMemoryPoWTimestamp;
+ /** Max reward shares by block height */
+ public static class MaxRewardSharesByTimestamp {
+ public long timestamp;
+ public int maxShares;
+ }
+ private List maxRewardSharesByTimestamp;
+
/** Settings relating to CIYAM AT feature. */
public static class CiyamAtSettings {
/** Fee per step/op-code executed. */
@@ -383,8 +391,8 @@ public class BlockChain {
return this.minAccountLevelToRewardShare;
}
- public int getMaxRewardSharesPerMintingAccount() {
- return this.maxRewardSharesPerMintingAccount;
+ public int getMaxRewardSharesPerFounderMintingAccount() {
+ return this.maxRewardSharesPerFounderMintingAccount;
}
public int getFounderEffectiveMintingLevel() {
@@ -417,6 +425,10 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue();
}
+ public long getRewardShareLimitTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.rewardShareLimitTimestamp.name()).longValue();
+ }
+
public long getCalcChainWeightTimestamp() {
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
}
@@ -465,6 +477,14 @@ public class BlockChain {
return this.getUnitFee();
}
+ public int getMaxRewardSharesAtTimestamp(long ourTimestamp) {
+ for (int i = maxRewardSharesByTimestamp.size() - 1; i >= 0; --i)
+ if (maxRewardSharesByTimestamp.get(i).timestamp <= ourTimestamp)
+ return maxRewardSharesByTimestamp.get(i).maxShares;
+
+ return 0;
+ }
+
/** Validate blockchain config read from JSON */
private void validateConfig() {
if (this.genesisInfo == null)
diff --git a/src/main/java/org/qortal/controller/AutoUpdate.java b/src/main/java/org/qortal/controller/AutoUpdate.java
index f07e82d1..2ec7c94a 100644
--- a/src/main/java/org/qortal/controller/AutoUpdate.java
+++ b/src/main/java/org/qortal/controller/AutoUpdate.java
@@ -15,6 +15,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -40,6 +41,7 @@ public class AutoUpdate extends Thread {
public static final String JAR_FILENAME = "qortal.jar";
public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME;
+ public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class);
private static final long CHECK_INTERVAL = 20 * 60 * 1000L; // ms
@@ -243,6 +245,11 @@ public class AutoUpdate extends Thread {
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
+ // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
+ javaCmd = javaCmd.stream()
+ .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
+ .collect(Collectors.toList());
+
// Remove JNI options as they won't be supported by command-line 'java'
// These are typically added by the AdvancedInstaller Java launcher EXE
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
@@ -261,10 +268,19 @@ public class AutoUpdate extends Thread {
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
MessageType.INFO);
- new ProcessBuilder(javaCmd).start();
+ ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
+
+ // New process will inherit our stdout and stderr
+ processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+ processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ Process process = processBuilder.start();
+
+ // Nothing to pipe to new process, so close output stream (process's stdin)
+ process.getOutputStream().close();
return true; // applying update OK
- } catch (IOException e) {
+ } catch (Exception e) {
LOGGER.error(String.format("Failed to apply update: %s", e.getMessage()));
try {
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index b333bd34..727fdd89 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -819,7 +819,7 @@ public class Controller extends Thread {
actionText = String.format("%s", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"));
SysTray.getInstance().setTrayIcon(3);
}
- else if (OnlineAccountsManager.getInstance().hasOnlineAccounts()) {
+ else if (OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures()) {
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
SysTray.getInstance().setTrayIcon(2);
}
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
index d6ff19d5..abd616e7 100644
--- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java
+++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
@@ -670,7 +670,7 @@ public class OnlineAccountsManager {
final Long minLatestBlockTimestamp = NTP.getTime() - (2 * 60 * 60 * 1000L);
boolean isUpToDate = Controller.getInstance().isUpToDate(minLatestBlockTimestamp);
- return isUpToDate && hasOnlineAccounts();
+ return isUpToDate && hasOurOnlineAccounts();
}
public boolean hasOurOnlineAccounts() {
diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java
index c1d31e31..281f34f1 100644
--- a/src/main/java/org/qortal/repository/AccountRepository.java
+++ b/src/main/java/org/qortal/repository/AccountRepository.java
@@ -159,6 +159,9 @@ public interface AccountRepository {
/** Returns number of active reward-shares involving passed public key as the minting account only. */
public int countRewardShares(byte[] mintingAccountPublicKey) throws DataException;
+ /** Returns number of active self-shares involving passed public key as the minting account only. */
+ public int countSelfShares(byte[] mintingAccountPublicKey) throws DataException;
+
public List getRewardShares() throws DataException;
public List findRewardShares(List mintingAccounts, List recipientAccounts, List involvedAddresses, Integer limit, Integer offset, Boolean reverse) throws DataException;
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java
index 028f3d46..9fdb0a3f 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java
@@ -688,6 +688,17 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
+ @Override
+ public int countSelfShares(byte[] minterPublicKey) throws DataException {
+ String sql = "SELECT COUNT(*) FROM RewardShares WHERE minter_public_key = ? AND minter = recipient";
+
+ try (ResultSet resultSet = this.repository.checkedExecute(sql, minterPublicKey)) {
+ return resultSet.getInt(1);
+ } catch (SQLException e) {
+ throw new DataException("Unable to count self-shares in repository", e);
+ }
+ }
+
@Override
public List getRewardShares() throws DataException {
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares";
diff --git a/src/main/java/org/qortal/transaction/RewardShareTransaction.java b/src/main/java/org/qortal/transaction/RewardShareTransaction.java
index be68196d..ed5029b2 100644
--- a/src/main/java/org/qortal/transaction/RewardShareTransaction.java
+++ b/src/main/java/org/qortal/transaction/RewardShareTransaction.java
@@ -140,8 +140,21 @@ public class RewardShareTransaction extends Transaction {
// Check the minting account hasn't reach maximum number of reward-shares
int rewardShareCount = this.repository.getAccountRepository().countRewardShares(creator.getPublicKey());
- if (rewardShareCount >= BlockChain.getInstance().getMaxRewardSharesPerMintingAccount())
+ int selfShareCount = this.repository.getAccountRepository().countSelfShares(creator.getPublicKey());
+
+ int maxRewardShares = BlockChain.getInstance().getMaxRewardSharesAtTimestamp(this.rewardShareTransactionData.getTimestamp());
+ if (creator.isFounder())
+ // Founders have a different limit
+ maxRewardShares = BlockChain.getInstance().getMaxRewardSharesPerFounderMintingAccount();
+
+ if (rewardShareCount >= maxRewardShares)
return ValidationResult.MAXIMUM_REWARD_SHARES;
+
+ // When filling all reward share slots, one must be a self share (after feature trigger timestamp)
+ if (this.rewardShareTransactionData.getTimestamp() >= BlockChain.getInstance().getRewardShareLimitTimestamp())
+ if (!isRecipientAlsoMinter && rewardShareCount == maxRewardShares-1 && selfShareCount == 0)
+ return ValidationResult.MAXIMUM_REWARD_SHARES;
+
} else {
// This transaction intends to modify/terminate an existing reward-share
diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json
index d4a9124d..03169723 100644
--- a/src/main/resources/blockchain.json
+++ b/src/main/resources/blockchain.json
@@ -15,7 +15,11 @@
"minAccountLevelToMint": 1,
"minAccountLevelForBlockSubmissions": 5,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 6,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 1657382400000, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 43200000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -59,6 +63,7 @@
"atFindNextTransactionFix": 275000,
"newBlockSigHeight": 320000,
"shareBinFix": 399000,
+ "rewardShareLimitTimestamp": 1657382400000,
"calcChainWeightTimestamp": 1620579600000,
"transactionV5Timestamp": 1642176000000,
"transactionV6Timestamp": 9999999999999,
diff --git a/src/test/java/org/qortal/test/common/AccountUtils.java b/src/test/java/org/qortal/test/common/AccountUtils.java
index bda1ae61..ffc4a7a1 100644
--- a/src/test/java/org/qortal/test/common/AccountUtils.java
+++ b/src/test/java/org/qortal/test/common/AccountUtils.java
@@ -41,7 +41,10 @@ public class AccountUtils {
public static TransactionData createRewardShare(Repository repository, String minter, String recipient, int sharePercent) throws DataException {
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, minter);
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
+ return createRewardShare(repository, mintingAccount, recipientAccount, sharePercent);
+ }
+ public static TransactionData createRewardShare(Repository repository, PrivateKeyAccount mintingAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException {
byte[] reference = mintingAccount.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
@@ -66,6 +69,15 @@ public class AccountUtils {
return rewardSharePrivateKey;
}
+ public static byte[] rewardShare(Repository repository, PrivateKeyAccount minterAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException {
+ TransactionData transactionData = createRewardShare(repository, minterAccount, recipientAccount, sharePercent);
+
+ TransactionUtils.signAndMint(repository, transactionData, minterAccount);
+ byte[] rewardSharePrivateKey = minterAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey());
+
+ return rewardSharePrivateKey;
+ }
+
public static Map> getBalances(Repository repository, long... assetIds) throws DataException {
Map> balances = new HashMap<>();
diff --git a/src/test/java/org/qortal/test/common/Common.java b/src/test/java/org/qortal/test/common/Common.java
index cb782343..3270a795 100644
--- a/src/test/java/org/qortal/test/common/Common.java
+++ b/src/test/java/org/qortal/test/common/Common.java
@@ -7,6 +7,7 @@ import java.math.BigDecimal;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.security.SecureRandom;
import java.security.Security;
import java.util.ArrayList;
import java.util.Collections;
@@ -25,6 +26,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.AfterClass;
import org.junit.BeforeClass;
+import org.qortal.account.PrivateKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.asset.AssetData;
@@ -111,6 +113,12 @@ public class Common {
return testAccountsByName.values().stream().map(account -> new TestAccount(repository, account)).collect(Collectors.toList());
}
+ public static PrivateKeyAccount generateRandomSeedAccount(Repository repository) {
+ byte[] seed = new byte[32];
+ new SecureRandom().nextBytes(seed);
+ return new PrivateKeyAccount(repository, seed);
+ }
+
public static void useSettingsAndDb(String settingsFilename, boolean dbInMemory) throws DataException {
closeRepository();
diff --git a/src/test/java/org/qortal/test/minting/RewardShareTests.java b/src/test/java/org/qortal/test/minting/RewardShareTests.java
index cde3c2ff..b5ac5e59 100644
--- a/src/test/java/org/qortal/test/minting/RewardShareTests.java
+++ b/src/test/java/org/qortal/test/minting/RewardShareTests.java
@@ -177,4 +177,143 @@ public class RewardShareTests extends Common {
}
}
+ @Test
+ public void testCreateRewardSharesBeforeReduction() throws DataException {
+ final int sharePercent = 0;
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Create 6 reward shares
+ for (int i=0; i<6; i++) {
+ AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ }
+
+ // 7th reward share should fail because we've reached the limit (and we're not yet requiring a self share)
+ AssertionError assertionError = null;
+ try {
+ AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ } catch (AssertionError e) {
+ assertionError = e;
+ }
+ assertNotNull("Transaction should be invalid", assertionError);
+ assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES"));
+ }
+ }
+
+ @Test
+ public void testCreateRewardSharesAfterReduction() throws DataException {
+ Common.useSettings("test-settings-v2-reward-shares.json");
+
+ final int sharePercent = 0;
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Create 2 reward shares
+ for (int i=0; i<2; i++) {
+ AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ }
+
+ // 3rd reward share should fail because we've reached the limit (and we haven't got a self share)
+ AssertionError assertionError = null;
+ try {
+ AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ } catch (AssertionError e) {
+ assertionError = e;
+ }
+ assertNotNull("Transaction should be invalid", assertionError);
+ assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES"));
+ }
+ }
+
+ @Test
+ public void testCreateSelfAndRewardSharesAfterReduction() throws DataException {
+ Common.useSettings("test-settings-v2-reward-shares.json");
+
+ final int sharePercent = 0;
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Create 2 reward shares
+ for (int i=0; i<2; i++) {
+ AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ }
+
+ // 3rd reward share should fail because we've reached the limit (and we haven't got a self share)
+ AssertionError assertionError = null;
+ try {
+ AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ } catch (AssertionError e) {
+ assertionError = e;
+ }
+ assertNotNull("Transaction should be invalid", assertionError);
+ assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES"));
+
+ // Now create a self share, which should succeed as we have space for it
+ AccountUtils.rewardShare(repository, dilbertAccount, dilbertAccount, sharePercent);
+
+ // 4th reward share should fail because we've reached the limit (including the self share)
+ assertionError = null;
+ try {
+ AccountUtils.rewardShare(repository, dilbertAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ } catch (AssertionError e) {
+ assertionError = e;
+ }
+ assertNotNull("Transaction should be invalid", assertionError);
+ assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES"));
+ }
+ }
+
+ @Test
+ public void testCreateFounderRewardSharesBeforeReduction() throws DataException {
+ final int sharePercent = 0;
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount aliceFounderAccount = Common.getTestAccount(repository, "alice");
+
+ // Create 5 reward shares (not 6, because alice already starts with a self reward share in the genesis block)
+ for (int i=0; i<5; i++) {
+ AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ }
+
+ // 6th reward share should fail
+ AssertionError assertionError = null;
+ try {
+ AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ } catch (AssertionError e) {
+ assertionError = e;
+ }
+ assertNotNull("Transaction should be invalid", assertionError);
+ assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES"));
+ }
+ }
+
+ @Test
+ public void testCreateFounderRewardSharesAfterReduction() throws DataException {
+ Common.useSettings("test-settings-v2-reward-shares.json");
+
+ final int sharePercent = 0;
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount aliceFounderAccount = Common.getTestAccount(repository, "alice");
+
+ // Create 5 reward shares (not 6, because alice already starts with a self reward share in the genesis block)
+ for (int i=0; i<5; i++) {
+ AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ }
+
+ // 6th reward share should fail
+ AssertionError assertionError = null;
+ try {
+ AccountUtils.rewardShare(repository, aliceFounderAccount, Common.generateRandomSeedAccount(repository), sharePercent);
+ } catch (AssertionError e) {
+ assertionError = e;
+ }
+ assertNotNull("Transaction should be invalid", assertionError);
+ assertTrue("Transaction should be invalid due to reaching maximum reward shares", assertionError.getMessage().contains("MAXIMUM_REWARD_SHARES"));
+ }
+ }
+
}
diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json
index 3fa8a1e5..38a18a8c 100644
--- a/src/test/resources/test-chain-v2-block-timestamps.json
+++ b/src/test/resources/test-chain-v2-block-timestamps.json
@@ -52,6 +52,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 9999999999999,
diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json
index 64aecbb6..648e91b5 100644
--- a/src/test/resources/test-chain-v2-disable-reference.json
+++ b/src/test/resources/test-chain-v2-disable-reference.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -51,6 +55,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json
index 30b1ffdd..5a36039c 100644
--- a/src/test/resources/test-chain-v2-founder-rewards.json
+++ b/src/test/resources/test-chain-v2-founder-rewards.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json
index bb94231a..2d53f421 100644
--- a/src/test/resources/test-chain-v2-leftover-reward.json
+++ b/src/test/resources/test-chain-v2-leftover-reward.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json
index 4c1c51b9..87d4efac 100644
--- a/src/test/resources/test-chain-v2-minting.json
+++ b/src/test/resources/test-chain-v2-minting.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json
index cfeac824..4d9becf3 100644
--- a/src/test/resources/test-chain-v2-qora-holder-extremes.json
+++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json
index 71d05767..5c9002da 100644
--- a/src/test/resources/test-chain-v2-qora-holder.json
+++ b/src/test/resources/test-chain-v2-qora-holder.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json
index eda61338..90c80c63 100644
--- a/src/test/resources/test-chain-v2-reward-levels.json
+++ b/src/test/resources/test-chain-v2-reward-levels.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 20 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 6,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json
index dafd3a51..228c76f6 100644
--- a/src/test/resources/test-chain-v2-reward-scaling.json
+++ b/src/test/resources/test-chain-v2-reward-scaling.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 20 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json
new file mode 100644
index 00000000..9e713095
--- /dev/null
+++ b/src/test/resources/test-chain-v2-reward-shares.json
@@ -0,0 +1,91 @@
+{
+ "isTestChain": true,
+ "blockTimestampMargin": 500,
+ "transactionExpiryPeriod": 86400000,
+ "maxBlockSize": 2097152,
+ "maxBytesPerUnitFee": 1024,
+ "unitFee": "0.1",
+ "nameRegistrationUnitFees": [
+ { "timestamp": 1645372800000, "fee": "5" }
+ ],
+ "requireGroupForApproval": false,
+ "minAccountLevelToRewardShare": 5,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 1655460000000, "maxShares": 3 }
+ ],
+ "founderEffectiveMintingLevel": 10,
+ "onlineAccountSignaturesMinLifetime": 3600000,
+ "onlineAccountSignaturesMaxLifetime": 86400000,
+ "rewardsByHeight": [
+ { "height": 1, "reward": 100 },
+ { "height": 11, "reward": 10 },
+ { "height": 21, "reward": 1 }
+ ],
+ "sharesByLevel": [
+ { "levels": [ 1, 2 ], "share": 0.05 },
+ { "levels": [ 3, 4 ], "share": 0.10 },
+ { "levels": [ 5, 6 ], "share": 0.15 },
+ { "levels": [ 7, 8 ], "share": 0.20 },
+ { "levels": [ 9, 10 ], "share": 0.25 }
+ ],
+ "qoraHoldersShare": 0.20,
+ "qoraPerQortReward": 250,
+ "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
+ "blockTimingsByHeight": [
+ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
+ ],
+ "ciyamAtSettings": {
+ "feePerStep": "0.0001",
+ "maxStepsPerRound": 500,
+ "stepsPerFunctionCall": 10,
+ "minutesPerBlock": 1
+ },
+ "featureTriggers": {
+ "messageHeight": 0,
+ "atHeight": 0,
+ "assetsTimestamp": 0,
+ "votingTimestamp": 0,
+ "arbitraryTimestamp": 0,
+ "powfixTimestamp": 0,
+ "qortalTimestamp": 0,
+ "newAssetPricingTimestamp": 0,
+ "groupApprovalTimestamp": 0,
+ "atFindNextTransactionFix": 0,
+ "newBlockSigHeight": 999999,
+ "shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 0,
+ "calcChainWeightTimestamp": 0,
+ "newConsensusTimestamp": 0,
+ "transactionV5Timestamp": 0,
+ "transactionV6Timestamp": 0,
+ "disableReferenceTimestamp": 9999999999999,
+ "aggregateSignatureTimestamp": 0
+ },
+ "genesisInfo": {
+ "version": 4,
+ "timestamp": 0,
+ "transactions": [
+ { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 },
+ { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
+ { "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
+
+ { "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
+ { "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
+ { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
+ { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
+
+ { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
+
+ { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
+ { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
+ { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
+
+ { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
+ { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": "100" },
+
+ { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 }
+ ]
+ }
+}
diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json
index 114cf91e..b2548f41 100644
--- a/src/test/resources/test-chain-v2.json
+++ b/src/test/resources/test-chain-v2.json
@@ -10,7 +10,11 @@
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
- "maxRewardSharesPerMintingAccount": 20,
+ "maxRewardSharesPerFounderMintingAccount": 6,
+ "maxRewardSharesByTimestamp": [
+ { "timestamp": 0, "maxShares": 6 },
+ { "timestamp": 9999999999999, "maxShares": 3 }
+ ],
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
@@ -52,6 +56,7 @@
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
+ "rewardShareLimitTimestamp": 9999999999999,
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
diff --git a/src/test/resources/test-settings-v2-reward-shares.json b/src/test/resources/test-settings-v2-reward-shares.json
new file mode 100644
index 00000000..092ebcc8
--- /dev/null
+++ b/src/test/resources/test-settings-v2-reward-shares.json
@@ -0,0 +1,19 @@
+{
+ "repositoryPath": "testdb",
+ "bitcoinNet": "TEST3",
+ "litecoinNet": "TEST3",
+ "restrictedApi": false,
+ "blockchainConfig": "src/test/resources/test-chain-v2-reward-shares.json",
+ "exportPath": "qortal-backup-test",
+ "bootstrap": false,
+ "wipeUnconfirmedOnStart": false,
+ "testNtpOffset": 0,
+ "minPeers": 0,
+ "pruneBlockLimit": 100,
+ "bootstrapFilenamePrefix": "test-",
+ "dataPath": "data-test",
+ "tempDataPath": "data-test/_temp",
+ "listsPath": "lists-test",
+ "storagePolicy": "FOLLOWED_OR_VIEWED",
+ "maxStorageCapacity": 104857600
+}