diff --git a/pom.xml b/pom.xml
index 30ad1e04..5ba103e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 4.2.2
+ 4.2.4
jar
true
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index e030e6a6..cf227d4a 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -130,6 +130,9 @@ public class Block {
/** Locally-generated AT fees */
protected long ourAtFees; // Generated locally
+ /** Cached online accounts validation decision, to avoid revalidating when true */
+ private boolean onlineAccountsAlreadyValid = false;
+
@FunctionalInterface
private interface BlockRewardDistributor {
long distribute(long amount, Map balanceChanges) throws DataException;
@@ -563,6 +566,13 @@ public class Block {
}
+ /**
+ * Force online accounts to be revalidated, e.g. at final stage of block minting.
+ */
+ public void clearOnlineAccountsValidationCache() {
+ this.onlineAccountsAlreadyValid = false;
+ }
+
// More information
/**
@@ -1043,6 +1053,10 @@ public class Block {
if (this.blockData.getHeight() != null && this.blockData.getHeight() == 1)
return ValidationResult.OK;
+ // Don't bother revalidating if accounts have already been validated in this block
+ if (this.onlineAccountsAlreadyValid)
+ return ValidationResult.OK;
+
// Expand block's online accounts indexes into actual accounts
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
// We use count of online accounts to validate decoded account indexes
@@ -1130,6 +1144,9 @@ public class Block {
// All online accounts valid, so save our list of online accounts for potential later use
this.cachedOnlineRewardShares = onlineRewardShares;
+ // Remember that the accounts are valid, to speed up subsequent checks
+ this.onlineAccountsAlreadyValid = true;
+
return ValidationResult.OK;
}
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index 218fb14d..d79203da 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -48,9 +48,6 @@ public class BlockChain {
/** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */
private long transactionExpiryPeriod;
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- private long unitFee;
-
private int maxBytesPerUnitFee;
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
@@ -89,6 +86,7 @@ public class BlockChain {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fee;
}
+ private List unitFees;
private List nameRegistrationUnitFees;
/** Map of which blockchain features are enabled when (height/timestamp) */
@@ -346,10 +344,6 @@ public class BlockChain {
return this.isTestChain;
}
- public long getUnitFee() {
- return this.unitFee;
- }
-
public int getMaxBytesPerUnitFee() {
return this.maxBytesPerUnitFee;
}
@@ -547,13 +541,22 @@ public class BlockChain {
throw new IllegalStateException(String.format("No block timing info available for height %d", ourHeight));
}
+ public long getUnitFeeAtTimestamp(long ourTimestamp) {
+ for (int i = unitFees.size() - 1; i >= 0; --i)
+ if (unitFees.get(i).timestamp <= ourTimestamp)
+ return unitFees.get(i).fee;
+
+ // Shouldn't happen, but set a sensible default just in case
+ return 100000;
+ }
+
public long getNameRegistrationUnitFeeAtTimestamp(long ourTimestamp) {
for (int i = nameRegistrationUnitFees.size() - 1; i >= 0; --i)
if (nameRegistrationUnitFees.get(i).timestamp <= ourTimestamp)
return nameRegistrationUnitFees.get(i).fee;
- // Default to system-wide unit fee
- return this.getUnitFee();
+ // Shouldn't happen, but set a sensible default just in case
+ return 100000;
}
public int getMaxRewardSharesAtTimestamp(long ourTimestamp) {
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index b1ed7e3c..35c89778 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -562,6 +562,9 @@ public class BlockMinter extends Thread {
// Sign to create block's signature
newBlock.sign();
+ // Ensure online accounts are fully re-validated in this final check
+ newBlock.clearOnlineAccountsValidationCache();
+
// Is newBlock still valid?
ValidationResult validationResult = newBlock.isValid();
if (validationResult != ValidationResult.OK)
diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java
index 2dad62e7..804bacbb 100644
--- a/src/main/java/org/qortal/controller/Synchronizer.java
+++ b/src/main/java/org/qortal/controller/Synchronizer.java
@@ -229,13 +229,6 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasOldVersion);
checkRecoveryModeForPeers(peers);
- if (recoveryMode) {
- // Needs a mutable copy of the unmodifiableList
- peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
- peers.removeIf(Controller.hasOnlyGenesisBlock);
- peers.removeIf(Controller.hasMisbehaved);
- peers.removeIf(Controller.hasOldVersion);
- }
// Check we have enough peers to potentially synchronize
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
@@ -262,10 +255,7 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasInferiorChainTip);
// Remove any peers that are no longer on a recent block since the last check
- // Except for times when we're in recovery mode, in which case we need to keep them
- if (!recoveryMode) {
- peers.removeIf(Controller.hasNoRecentBlock);
- }
+ peers.removeIf(Controller.hasNoRecentBlock);
final int peersRemoved = peersBeforeComparison - peers.size();
if (peersRemoved > 0 && peers.size() > 0)
@@ -1340,8 +1330,8 @@ public class Synchronizer extends Thread {
return SynchronizationResult.INVALID_DATA;
}
- // Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
- if (!recoveryMode && peer.getChainTipData() != null) {
+ // Final check to make sure the peer isn't out of date
+ if (peer.getChainTipData() != null) {
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
diff --git a/src/main/java/org/qortal/controller/TransactionImporter.java b/src/main/java/org/qortal/controller/TransactionImporter.java
index 5c70f369..fa35cbbe 100644
--- a/src/main/java/org/qortal/controller/TransactionImporter.java
+++ b/src/main/java/org/qortal/controller/TransactionImporter.java
@@ -47,6 +47,9 @@ public class TransactionImporter extends Thread {
/** Map of recent invalid unconfirmed transactions. Key is base58 transaction signature, value is do-not-request expiry timestamp. */
private final Map invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>());
+ /** Cached list of unconfirmed transactions, used when counting per creator. This is replaced regularly */
+ public static List unconfirmedTransactionsCache = null;
+
public static synchronized TransactionImporter getInstance() {
if (instance == null) {
@@ -254,6 +257,12 @@ public class TransactionImporter extends Thread {
int processedCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
+ // Use a single copy of the unconfirmed transactions list for each cycle, to speed up constant lookups
+ // when counting unconfirmed transactions by creator.
+ List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
+ unconfirmedTransactions.removeIf(t -> t.getType() == Transaction.TransactionType.CHAT);
+ unconfirmedTransactionsCache = unconfirmedTransactions;
+
// Import transactions with valid signatures
try {
for (int i = 0; i < sigValidTransactions.size(); ++i) {
@@ -286,6 +295,11 @@ public class TransactionImporter extends Thread {
case OK: {
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
+
+ // Add to the unconfirmed transactions cache
+ if (transactionData.getType() != Transaction.TransactionType.CHAT && unconfirmedTransactionsCache != null) {
+ unconfirmedTransactionsCache.add(transactionData);
+ }
break;
}
@@ -317,6 +331,9 @@ public class TransactionImporter extends Thread {
} finally {
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
blockchainLock.unlock();
+
+ // Clear the unconfirmed transaction cache so new data can be populated in the next cycle
+ unconfirmedTransactionsCache = null;
}
} catch (DataException e) {
LOGGER.error("Repository issue while importing incoming transactions", e);
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index ac9b8857..8d4eab56 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -227,7 +227,7 @@ public class Settings {
private int maxRetries = 2;
/** The number of seconds of no activity before recovery mode begins */
- public long recoveryModeTimeout = 24 * 60 * 60 * 1000L;
+ public long recoveryModeTimeout = 9999999999999L;
/** Minimum peer version number required in order to sync with them */
private String minPeerVersion = "4.1.2";
diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java
index bd91f25a..f750aff5 100644
--- a/src/main/java/org/qortal/transaction/Transaction.java
+++ b/src/main/java/org/qortal/transaction/Transaction.java
@@ -13,6 +13,7 @@ import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.controller.Controller;
+import org.qortal.controller.TransactionImporter;
import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData;
import org.qortal.data.group.GroupApprovalData;
@@ -377,7 +378,7 @@ public abstract class Transaction {
* @return
*/
public long getUnitFee(Long timestamp) {
- return BlockChain.getInstance().getUnitFee();
+ return BlockChain.getInstance().getUnitFeeAtTimestamp(timestamp);
}
/**
@@ -617,7 +618,10 @@ public abstract class Transaction {
}
private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException {
- List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
+ List unconfirmedTransactions = TransactionImporter.getInstance().unconfirmedTransactionsCache;
+ if (unconfirmedTransactions == null) {
+ unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
+ }
// We exclude CHAT transactions as they never get included into blocks and
// have spam/DoS prevention by requiring proof of work
diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json
index c6151204..1cf4f010 100644
--- a/src/main/resources/blockchain.json
+++ b/src/main/resources/blockchain.json
@@ -3,8 +3,12 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.001",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.001" },
+ { "timestamp": 1692118800000, "fee": "0.01" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.001" },
{ "timestamp": 1645372800000, "fee": "5" },
{ "timestamp": 1651420800000, "fee": "1.25" }
],
diff --git a/src/test/java/org/qortal/test/MessageTests.java b/src/test/java/org/qortal/test/MessageTests.java
index f08c7b2f..4d0ecfcc 100644
--- a/src/test/java/org/qortal/test/MessageTests.java
+++ b/src/test/java/org/qortal/test/MessageTests.java
@@ -85,7 +85,7 @@ public class MessageTests extends Common {
byte[] randomReference = new byte[64];
random.nextBytes(randomReference);
- long minimumFee = BlockChain.getInstance().getUnitFee();
+ long minimumFee = BlockChain.getInstance().getUnitFeeAtTimestamp(System.currentTimeMillis());
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
diff --git a/src/test/java/org/qortal/test/common/transaction/TestTransaction.java b/src/test/java/org/qortal/test/common/transaction/TestTransaction.java
index 11fdf58e..b580ecd3 100644
--- a/src/test/java/org/qortal/test/common/transaction/TestTransaction.java
+++ b/src/test/java/org/qortal/test/common/transaction/TestTransaction.java
@@ -7,13 +7,15 @@ import org.qortal.block.BlockChain;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
+import org.qortal.utils.NTP;
public abstract class TestTransaction {
protected static final Random random = new Random();
public static BaseTransactionData generateBase(PrivateKeyAccount account, int txGroupId) throws DataException {
- return new BaseTransactionData(System.currentTimeMillis(), txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
+ long timestamp = System.currentTimeMillis();
+ return new BaseTransactionData(timestamp, txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFeeAtTimestamp(timestamp), null);
}
public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException {
diff --git a/src/test/java/org/qortal/test/naming/BuySellTests.java b/src/test/java/org/qortal/test/naming/BuySellTests.java
index 4530820e..f0e97b94 100644
--- a/src/test/java/org/qortal/test/naming/BuySellTests.java
+++ b/src/test/java/org/qortal/test/naming/BuySellTests.java
@@ -44,7 +44,7 @@ public class BuySellTests extends Common {
bob = Common.getTestAccount(repository, "bob");
name = "test name" + " " + random.nextInt(1_000_000);
- price = random.nextInt(1000) * Amounts.MULTIPLIER;
+ price = (random.nextInt(1000) + 1) * Amounts.MULTIPLIER;
}
@After
diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java
index 2bcd098d..91eb3dc9 100644
--- a/src/test/java/org/qortal/test/naming/MiscTests.java
+++ b/src/test/java/org/qortal/test/naming/MiscTests.java
@@ -20,6 +20,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.*;
import org.qortal.test.common.transaction.TestTransaction;
+import org.qortal.transaction.PaymentTransaction;
import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -329,15 +330,19 @@ public class MiscTests extends Common {
public void testRegisterNameFeeIncrease() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
- // Set nameRegistrationUnitFeeTimestamp to a time far in the future
+ // Add original fee to nameRegistrationUnitFees
+ UnitFeesByTimestamp originalFee = new UnitFeesByTimestamp();
+ originalFee.timestamp = 0L;
+ originalFee.fee = new AmountTypeAdapter().unmarshal("0.1");
+
+ // Add a time far in the future to nameRegistrationUnitFees
UnitFeesByTimestamp futureFeeIncrease = new UnitFeesByTimestamp();
futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("5");
- FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(futureFeeIncrease), true);
+ FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(originalFee, futureFeeIncrease), true);
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Validate unit fees pre and post timestamp
- assertEquals(10000000, BlockChain.getInstance().getUnitFee()); // 0.1 QORT
assertEquals(10000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp - 1)); // 0.1 QORT
assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); // 5 QORT
@@ -362,7 +367,7 @@ public class MiscTests extends Common {
futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10");
- FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(pastFeeIncrease, futureFeeIncrease), true);
+ FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(originalFee, pastFeeIncrease, futureFeeIncrease), true);
assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(pastFeeIncrease.timestamp));
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
@@ -387,4 +392,123 @@ public class MiscTests extends Common {
}
}
+ // test reading the name registration fee schedule from blockchain.json / test-chain-v2.json
+ @Test
+ public void testRegisterNameFeeScheduleInTestchainData() throws Exception {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ final long expectedFutureFeeIncreaseTimestamp = 9999999999999L; // 20 Nov 2286, as per test-chain-v2.json
+ final long expectedFutureFeeIncreaseValue = new AmountTypeAdapter().unmarshal("5");
+
+ assertEquals(expectedFutureFeeIncreaseValue, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp));
+
+ // Validate unit fees pre and post timestamp
+ assertEquals(10000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp - 1)); // 0.1 QORT
+ assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp)); // 5 QORT
+
+ // Register-name
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ String name = "test-name";
+ String data = "{\"age\":30}";
+
+ RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
+ transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
+ assertEquals(10000000L, transactionData.getFee().longValue());
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+ }
+ }
+
+
+
+ // test general unit fee increase
+ @Test
+ public void testUnitFeeIncrease() throws Exception {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ // Add original fee to unitFees
+ UnitFeesByTimestamp originalFee = new UnitFeesByTimestamp();
+ originalFee.timestamp = 0L;
+ originalFee.fee = new AmountTypeAdapter().unmarshal("0.1");
+
+ // Add a time far in the future to unitFees
+ UnitFeesByTimestamp futureFeeIncrease = new UnitFeesByTimestamp();
+ futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286
+ futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("1");
+ FieldUtils.writeField(BlockChain.getInstance(), "unitFees", Arrays.asList(originalFee, futureFeeIncrease), true);
+ assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
+
+ // Validate unit fees pre and post timestamp
+ assertEquals(10000000, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp - 1)); // 0.1 QORT
+ assertEquals(100000000, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); // 1 QORT
+
+ // Payment
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
+
+ PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000);
+ transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
+ assertEquals(10000000L, transactionData.getFee().longValue());
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+
+ // Set fee increase to a time in the past
+ Long now = NTP.getTime();
+ UnitFeesByTimestamp pastFeeIncrease = new UnitFeesByTimestamp();
+ pastFeeIncrease.timestamp = now - 1000L; // 1 second ago
+ pastFeeIncrease.fee = new AmountTypeAdapter().unmarshal("3");
+
+ // Set another increase in the future
+ futureFeeIncrease = new UnitFeesByTimestamp();
+ futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future
+ futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10");
+
+ FieldUtils.writeField(BlockChain.getInstance(), "unitFees", Arrays.asList(originalFee, pastFeeIncrease, futureFeeIncrease), true);
+ assertEquals(originalFee.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(originalFee.timestamp));
+ assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(pastFeeIncrease.timestamp));
+ assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
+
+ // Send another payment transaction
+ // Fee should be determined automatically
+ transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 50000);
+ assertEquals(300000000L, transactionData.getFee().longValue());
+ Transaction transaction = Transaction.fromData(repository, transactionData);
+ transaction.sign(alice);
+ ValidationResult result = transaction.importAsUnconfirmed();
+ assertTrue("Transaction should be valid", ValidationResult.OK == result);
+
+ // Now try fetching and setting fee manually
+ transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 50000);
+ transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
+ assertEquals(300000000L, transactionData.getFee().longValue());
+ transaction = Transaction.fromData(repository, transactionData);
+ transaction.sign(alice);
+ result = transaction.importAsUnconfirmed();
+ assertTrue("Transaction should be valid", ValidationResult.OK == result);
+ }
+ }
+
+ // test reading the fee schedule from blockchain.json / test-chain-v2.json
+ @Test
+ public void testFeeScheduleInTestchainData() throws Exception {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ final long expectedFutureFeeIncreaseTimestamp = 9999999999999L; // 20 Nov 2286, as per test-chain-v2.json
+ final long expectedFutureFeeIncreaseValue = new AmountTypeAdapter().unmarshal("1");
+
+ assertEquals(expectedFutureFeeIncreaseValue, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp));
+
+ // Validate unit fees pre and post timestamp
+ assertEquals(10000000, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp - 1)); // 0.1 QORT
+ assertEquals(100000000, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp)); // 1 QORT
+
+ // Payment
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
+
+ PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000);
+ transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
+ assertEquals(10000000L, transactionData.getFee().longValue());
+ TransactionUtils.signAndMint(repository, transactionData, alice);
+ }
+ }
+
}
diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json
index 3b4de702..25915e9a 100644
--- a/src/test/resources/test-chain-v2-block-timestamps.json
+++ b/src/test/resources/test-chain-v2-block-timestamps.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json
index c93fbb78..ae499491 100644
--- a/src/test/resources/test-chain-v2-disable-reference.json
+++ b/src/test/resources/test-chain-v2-disable-reference.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json
index 1b068932..e6b327b1 100644
--- a/src/test/resources/test-chain-v2-founder-rewards.json
+++ b/src/test/resources/test-chain-v2-founder-rewards.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json
index aef76cc2..9fda9b1f 100644
--- a/src/test/resources/test-chain-v2-leftover-reward.json
+++ b/src/test/resources/test-chain-v2-leftover-reward.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json
index db6d8a0b..4ff3ec3c 100644
--- a/src/test/resources/test-chain-v2-minting.json
+++ b/src/test/resources/test-chain-v2-minting.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
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 2452d4d2..306fd9a3 100644
--- a/src/test/resources/test-chain-v2-qora-holder-extremes.json
+++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json
index 23193729..6128baa7 100644
--- a/src/test/resources/test-chain-v2-qora-holder-reduction.json
+++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json
index 9d81632b..9b6bccda 100644
--- a/src/test/resources/test-chain-v2-qora-holder.json
+++ b/src/test/resources/test-chain-v2-qora-holder.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json
index 81609595..1ffa8baf 100644
--- a/src/test/resources/test-chain-v2-reward-levels.json
+++ b/src/test/resources/test-chain-v2-reward-levels.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json
index 21a5b7a7..6c3b3276 100644
--- a/src/test/resources/test-chain-v2-reward-scaling.json
+++ b/src/test/resources/test-chain-v2-reward-scaling.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json
index 6119ac48..09627e1f 100644
--- a/src/test/resources/test-chain-v2-reward-shares.json
+++ b/src/test/resources/test-chain-v2-reward-shares.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo.json b/src/test/resources/test-chain-v2-self-sponsorship-algo.json
index dc5f3961..81cda1e8 100644
--- a/src/test/resources/test-chain-v2-self-sponsorship-algo.json
+++ b/src/test/resources/test-chain-v2-self-sponsorship-algo.json
@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 0,
- "unitFee": "0.00000001",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.00000001" }
+ ],
"nameRegistrationUnitFees": [
+ { "timestamp": 0, "fee": "0.00000001" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,
diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json
index d0c460df..2e96e911 100644
--- a/src/test/resources/test-chain-v2.json
+++ b/src/test/resources/test-chain-v2.json
@@ -4,9 +4,13 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
- "unitFee": "0.1",
+ "unitFees": [
+ { "timestamp": 0, "fee": "0.1" },
+ { "timestamp": 9999999999999, "fee": "1" }
+ ],
"nameRegistrationUnitFees": [
- { "timestamp": 1645372800000, "fee": "5" }
+ { "timestamp": 0, "fee": "0.1" },
+ { "timestamp": 9999999999999, "fee": "5" }
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,