diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index cf227d4a..16c061da 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -84,6 +84,7 @@ public class Block { TRANSACTION_PROCESSING_FAILED(53), TRANSACTION_ALREADY_PROCESSED(54), TRANSACTION_NEEDS_APPROVAL(55), + TRANSACTION_NOT_CONFIRMABLE(56), AT_STATES_MISMATCH(61), ONLINE_ACCOUNTS_INVALID(70), ONLINE_ACCOUNT_UNKNOWN(71), @@ -1251,6 +1252,13 @@ public class Block { || transaction.getDeadline() <= this.blockData.getTimestamp()) return ValidationResult.TRANSACTION_TIMESTAMP_INVALID; + // After feature trigger, check that this transaction is confirmable + if (transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) { + if (!transaction.isConfirmable()) { + return ValidationResult.TRANSACTION_NOT_CONFIRMABLE; + } + } + // Check transaction isn't already included in a block if (this.repository.getTransactionRepository().isConfirmed(transactionData.getSignature())) return ValidationResult.TRANSACTION_ALREADY_PROCESSED; diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index d79203da..540e6cf4 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -209,6 +209,9 @@ public class BlockChain { /** Snapshot timestamp for self sponsorship algo V1 */ private long selfSponsorshipAlgoV1SnapshotTimestamp; + /** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */ + private long mempowTransactionUpdatesTimestamp; + /** Max reward shares by block height */ public static class MaxRewardSharesByTimestamp { public long timestamp; @@ -370,6 +373,11 @@ public class BlockChain { return this.selfSponsorshipAlgoV1SnapshotTimestamp; } + // Feature-trigger timestamp to modify behaviour of various transactions that support mempow + public long getMemPoWTransactionUpdatesTimestamp() { + return this.mempowTransactionUpdatesTimestamp; + } + /** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */ public boolean getRequireGroupForApproval() { return this.requireGroupForApproval; diff --git a/src/main/java/org/qortal/transaction/ArbitraryTransaction.java b/src/main/java/org/qortal/transaction/ArbitraryTransaction.java index 7034d7b8..a3f4827b 100644 --- a/src/main/java/org/qortal/transaction/ArbitraryTransaction.java +++ b/src/main/java/org/qortal/transaction/ArbitraryTransaction.java @@ -1,6 +1,5 @@ package org.qortal.transaction; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -8,7 +7,6 @@ import java.util.stream.Collectors; import org.qortal.account.Account; import org.qortal.block.BlockChain; import org.qortal.controller.arbitrary.ArbitraryDataManager; -import org.qortal.controller.arbitrary.ArbitraryDataStorageManager; import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.crypto.Crypto; import org.qortal.crypto.MemoryPoW; @@ -88,8 +86,14 @@ public class ArbitraryTransaction extends Transaction { if (this.transactionData.getFee() < 0) return ValidationResult.NEGATIVE_FEE; - // After the feature trigger, we require the fee to be sufficient if it's not 0. - // If the fee is zero, then the nonce is validated in isSignatureValid() as an alternative to a fee + // As of the mempow transaction updates timestamp, a nonce is no longer supported, so a valid fee must be included + if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) { + // Validate the fee + return super.isFeeValid(); + } + + // After the earlier "optional fee" feature trigger, we required the fee to be sufficient if it wasn't 0. + // If the fee was zero, then the nonce was validated in isSignatureValid() as an alternative to a fee if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) { return super.isFeeValid(); } @@ -214,7 +218,13 @@ public class ArbitraryTransaction extends Transaction { // Clear nonce from transactionBytes ArbitraryTransactionTransformer.clearNonce(transactionBytes); - // As of feature-trigger timestamp, we only require a nonce when the fee is zero + // As of the mempow transaction updates timestamp, a nonce is no longer supported, so a fee must be included + if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) { + // Require that the fee is a positive number. Fee checking itself is performed in isFeeValid() + return (this.arbitraryTransactionData.getFee() > 0L); + } + + // As of the earlier "optional fee" feature-trigger timestamp, we only required a nonce when the fee was zero boolean beforeFeatureTrigger = this.arbitraryTransactionData.getTimestamp() < BlockChain.getInstance().getArbitraryOptionalFeeTimestamp(); if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) { // We only need to check nonce for recent transactions due to PoW verification overhead diff --git a/src/main/java/org/qortal/transaction/ChatTransaction.java b/src/main/java/org/qortal/transaction/ChatTransaction.java index f6e26802..3d968461 100644 --- a/src/main/java/org/qortal/transaction/ChatTransaction.java +++ b/src/main/java/org/qortal/transaction/ChatTransaction.java @@ -148,6 +148,12 @@ public class ChatTransaction extends Transaction { // Nothing to do } + @Override + public boolean isConfirmable() { + // CHAT transactions can't go into blocks + return false; + } + @Override public ValidationResult isValid() throws DataException { // Nonce checking is done via isSignatureValid() as that method is only called once per import diff --git a/src/main/java/org/qortal/transaction/MessageTransaction.java b/src/main/java/org/qortal/transaction/MessageTransaction.java index a9d3a01c..b61c3d11 100644 --- a/src/main/java/org/qortal/transaction/MessageTransaction.java +++ b/src/main/java/org/qortal/transaction/MessageTransaction.java @@ -33,7 +33,9 @@ public class MessageTransaction extends Transaction { public static final int MAX_DATA_SIZE = 4000; public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes - public static final int POW_DIFFICULTY = 14; // leading zero bits + public static final int POW_DIFFICULTY_V1 = 14; // leading zero bits + public static final int POW_DIFFICULTY_V2_CONFIRMABLE = 16; // leading zero bits + public static final int POW_DIFFICULTY_V2_UNCONFIRMABLE = 12; // leading zero bits // Properties @@ -109,7 +111,17 @@ public class MessageTransaction extends Transaction { MessageTransactionTransformer.clearNonce(transactionBytes); // Calculate nonce - this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY)); + this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty())); + } + + public int getPoWDifficulty() { + // The difficulty changes at the "mempow transactions updates" timestamp + if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) { + // If this message is confirmable then require a higher difficulty + return this.isConfirmable() ? POW_DIFFICULTY_V2_CONFIRMABLE : POW_DIFFICULTY_V2_UNCONFIRMABLE; + } + // Before feature trigger timestamp, so use existing difficulty value + return POW_DIFFICULTY_V1; } /** @@ -183,6 +195,18 @@ public class MessageTransaction extends Transaction { return super.hasValidReference(); } + @Override + public boolean isConfirmable() { + // After feature trigger timestamp, only messages to an AT address can confirm + if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) { + if (this.messageTransactionData.getRecipient() == null || !this.messageTransactionData.getRecipient().toUpperCase().startsWith("A")) { + // Message isn't to an AT address, so this transaction is unconfirmable + return false; + } + } + return true; + } + @Override public ValidationResult isValid() throws DataException { // Nonce checking is done via isSignatureValid() as that method is only called once per import @@ -235,7 +259,7 @@ public class MessageTransaction extends Transaction { MessageTransactionTransformer.clearNonce(transactionBytes); // Check nonce - return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce); + return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty(), nonce); } @Override @@ -256,6 +280,11 @@ public class MessageTransaction extends Transaction { @Override public void process() throws DataException { + // Only certain MESSAGE transactions are able to confirm + if (!this.isConfirmable()) { + throw new DataException("Unconfirmable MESSAGE transactions should never be processed"); + } + // If we have no amount then there's nothing to do if (this.messageTransactionData.getAmount() == 0L) return; @@ -280,6 +309,11 @@ public class MessageTransaction extends Transaction { @Override public void orphan() throws DataException { + // Only certain MESSAGE transactions are able to confirm + if (!this.isConfirmable()) { + throw new DataException("Unconfirmable MESSAGE transactions should never be orphaned"); + } + // If we have no amount then there's nothing to do if (this.messageTransactionData.getAmount() == 0L) return; diff --git a/src/main/java/org/qortal/transaction/PresenceTransaction.java b/src/main/java/org/qortal/transaction/PresenceTransaction.java index 8076997c..56a9f633 100644 --- a/src/main/java/org/qortal/transaction/PresenceTransaction.java +++ b/src/main/java/org/qortal/transaction/PresenceTransaction.java @@ -155,6 +155,12 @@ public class PresenceTransaction extends Transaction { // Nothing to do } + @Override + public boolean isConfirmable() { + // PRESENCE transactions can't go into blocks + return false; + } + @Override public ValidationResult isValid() throws DataException { // Nonce checking is done via isSignatureValid() as that method is only called once per import diff --git a/src/main/java/org/qortal/transaction/PublicizeTransaction.java b/src/main/java/org/qortal/transaction/PublicizeTransaction.java index 76fef00b..44f93e6e 100644 --- a/src/main/java/org/qortal/transaction/PublicizeTransaction.java +++ b/src/main/java/org/qortal/transaction/PublicizeTransaction.java @@ -7,6 +7,7 @@ import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.MemoryPoW; import org.qortal.data.transaction.PublicizeTransactionData; import org.qortal.data.transaction.TransactionData; @@ -89,6 +90,12 @@ public class PublicizeTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { + // Disable completely after feature-trigger timestamp, at the same time that mempow difficulties are being increased. + // It could be enabled again in the future, but preferably with an enforced minimum fee instead of allowing a mempow nonce. + if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) { + return ValidationResult.NOT_SUPPORTED; + } + // There can be only one List signatures = this.repository.getTransactionRepository().getSignaturesMatchingCriteria( TransactionType.PUBLICIZE, diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index f750aff5..e0ed1f82 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -248,7 +248,8 @@ public abstract class Transaction { GROUP_APPROVAL_REQUIRED(98), ACCOUNT_NOT_TRANSFERABLE(99), INVALID_BUT_OK(999), - NOT_YET_RELEASED(1000); + NOT_YET_RELEASED(1000), + NOT_SUPPORTED(1001); public final int value; @@ -636,7 +637,7 @@ public abstract class Transaction { } /** - * Returns sorted, unconfirmed transactions, excluding invalid. + * Returns sorted, unconfirmed transactions, excluding invalid and unconfirmable. * * @return sorted, unconfirmed transactions * @throws DataException @@ -654,7 +655,8 @@ public abstract class Transaction { TransactionData transactionData = unconfirmedTransactionsIterator.next(); Transaction transaction = Transaction.fromData(repository, transactionData); - if (transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK) + // Must be confirmable and valid + if (!transaction.isConfirmable() || transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK) unconfirmedTransactionsIterator.remove(); } @@ -892,6 +894,17 @@ public abstract class Transaction { /* To be optionally overridden */ } + /** + * Returns whether transaction is 'confirmable' - i.e. is of a type that + * can be included in a block. Some transactions are 'unconfirmable' + * and therefore must remain in the mempool until they expire. + * @return + */ + public boolean isConfirmable() { + /* To be optionally overridden */ + return true; + } + /** * Returns whether transaction can be added to the blockchain. *

diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 1cf4f010..996ae2c1 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -29,6 +29,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 1659801600000, "selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000, + "mempowTransactionUpdatesTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 5.00 }, { "height": 259201, "reward": 4.75 }, diff --git a/src/test/java/org/qortal/test/MemoryPoWTests.java b/src/test/java/org/qortal/test/MemoryPoWTests.java index 662fab19..3b0045e5 100644 --- a/src/test/java/org/qortal/test/MemoryPoWTests.java +++ b/src/test/java/org/qortal/test/MemoryPoWTests.java @@ -3,6 +3,8 @@ package org.qortal.test; import org.junit.Ignore; import org.junit.Test; import org.qortal.crypto.MemoryPoW; +import org.qortal.repository.DataException; +import org.qortal.test.common.Common; import static org.junit.Assert.*; @@ -39,13 +41,14 @@ public class MemoryPoWTests { } @Test - public void testMultipleComputes() { + public void testMultipleComputes() throws DataException { + Common.useDefaultSettings(); Random random = new Random(); - final int sampleSize = 20; + final int sampleSize = 10; final long stddevDivisor = sampleSize * (sampleSize - 1); - for (int difficulty = 8; difficulty < 16; difficulty += 2) { + for (int difficulty = 8; difficulty <= 16; difficulty++) { byte[] data = new byte[256]; long[] times = new long[sampleSize]; diff --git a/src/test/java/org/qortal/test/MessageTests.java b/src/test/java/org/qortal/test/MessageTests.java index 4d0ecfcc..3e549235 100644 --- a/src/test/java/org/qortal/test/MessageTests.java +++ b/src/test/java/org/qortal/test/MessageTests.java @@ -1,5 +1,6 @@ package org.qortal.test; +import org.apache.commons.lang3.reflect.FieldUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -14,12 +15,9 @@ import org.qortal.group.Group.ApprovalThreshold; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; -import org.qortal.test.common.BlockUtils; -import org.qortal.test.common.Common; -import org.qortal.test.common.GroupUtils; -import org.qortal.test.common.TestAccount; -import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.*; import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.MessageTransaction; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.TransactionType; @@ -139,7 +137,7 @@ public class MessageTests extends Common { } @Test - public void withRecipentWithAmount() throws DataException { + public void withRecipientWithAmount() throws DataException { testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT); } @@ -153,6 +151,132 @@ public class MessageTests extends Common { testMessage(1, null, 0L, null); } + @Test + public void atRecipientNoFeeWithNonce() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String atRecipient = deployAt(); + MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true); + + // Transaction should be confirmable because it's to an AT, and therefore should be present in a block + assertTrue(transaction.isConfirmable()); + TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice); + assertTrue(isTransactionConfirmed(repository, transaction)); + assertEquals(16, transaction.getPoWDifficulty()); + + BlockUtils.orphanLastBlock(repository); + } + } + + @Test + public void regularRecipientNoFeeWithNonce() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + + MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true); + + // Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block + assertFalse(transaction.isConfirmable()); + TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice); + assertFalse(isTransactionConfirmed(repository, transaction)); + assertEquals(12, transaction.getPoWDifficulty()); + + BlockUtils.orphanLastBlock(repository); + } + } + + @Test + public void noRecipientNoFeeWithNonce() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + + MessageTransaction transaction = testFeeNonce(repository, false, true, null, true); + + // Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block + assertFalse(transaction.isConfirmable()); + TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice); + assertFalse(isTransactionConfirmed(repository, transaction)); + assertEquals(12, transaction.getPoWDifficulty()); + + BlockUtils.orphanLastBlock(repository); + } + } + + @Test + public void atRecipientWithFeeNoNonce() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String atRecipient = deployAt(); + MessageTransaction transaction = testFeeNonce(repository, true, false, atRecipient, true); + + // Transaction should be confirmable because it's to an AT, and therefore should be present in a block + assertTrue(transaction.isConfirmable()); + TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice); + assertTrue(isTransactionConfirmed(repository, transaction)); + assertEquals(16, transaction.getPoWDifficulty()); + + BlockUtils.orphanLastBlock(repository); + } + } + + @Test + public void regularRecipientWithFeeNoNonce() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + + MessageTransaction transaction = testFeeNonce(repository, true, false, recipient, true); + + // Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block + assertFalse(transaction.isConfirmable()); + TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice); + assertFalse(isTransactionConfirmed(repository, transaction)); + assertEquals(12, transaction.getPoWDifficulty()); + + BlockUtils.orphanLastBlock(repository); + } + } + + @Test + public void atRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key + FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true); + + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String atRecipient = deployAt(); + MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true); + + // Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger + assertTrue(transaction.isConfirmable()); + TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice); + assertTrue(isTransactionConfirmed(repository, transaction)); + assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases + + BlockUtils.orphanLastBlock(repository); + } + } + + @Test + public void regularRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key + FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true); + + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true); + + // Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger + assertTrue(transaction.isConfirmable()); + TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice); + assertTrue(isTransactionConfirmed(repository, transaction)); // All MESSAGE transactions would confirm before feature trigger + assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases + + BlockUtils.orphanLastBlock(repository); + } + } + @Test public void serializationTests() throws DataException, TransformationException { // with recipient, with amount @@ -165,6 +289,24 @@ public class MessageTests extends Common { testSerialization(null, 0L, null); } + private String deployAt() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + + String address = deployAtTransaction.getATAccount().getAddress(); + assertNotNull(address); + return address; + } + } + + private boolean isTransactionConfirmed(Repository repository, MessageTransaction transaction) throws DataException { + TransactionData queriedTransactionData = repository.getTransactionRepository().fromSignature(transaction.getTransactionData().getSignature()); + return queriedTransactionData.getBlockHeight() != null && queriedTransactionData.getBlockHeight() > 0; + } + private boolean isValid(int txGroupId, String recipient, long amount, Long assetId) throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { TestAccount alice = Common.getTestAccount(repository, "alice"); @@ -195,41 +337,48 @@ public class MessageTests extends Common { return messageTransaction.hasValidReference(); } - private void testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException { + + private MessageTransaction testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - TestAccount alice = Common.getTestAccount(repository, "alice"); - - int txGroupId = 0; - int nonce = 0; - long amount = 0; - long assetId = Asset.QORT; - byte[] data = new byte[1]; - boolean isText = false; - boolean isEncrypted = false; - - MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId), - version, nonce, recipient, amount, assetId, data, isText, isEncrypted); - - MessageTransaction transaction = new MessageTransaction(repository, transactionData); - - if (withFee) - transactionData.setFee(transaction.calcRecommendedFee()); - else - transactionData.setFee(0L); - - if (withNonce) { - transaction.computeNonce(); - } else { - transactionData.setNonce(-1); - } - - transaction.sign(alice); - - assertEquals(isValid, transaction.isSignatureValid()); + return testFeeNonce(repository, withFee, withNonce, recipient, isValid); } } - private void testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException { + private MessageTransaction testFeeNonce(Repository repository, boolean withFee, boolean withNonce, String recipient, boolean isValid) throws DataException { + TestAccount alice = Common.getTestAccount(repository, "alice"); + + int txGroupId = 0; + int nonce = 0; + long amount = 0; + long assetId = Asset.QORT; + byte[] data = new byte[1]; + boolean isText = false; + boolean isEncrypted = false; + + MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId), + version, nonce, recipient, amount, assetId, data, isText, isEncrypted); + + MessageTransaction transaction = new MessageTransaction(repository, transactionData); + + if (withFee) + transactionData.setFee(transaction.calcRecommendedFee()); + else + transactionData.setFee(0L); + + if (withNonce) { + transaction.computeNonce(); + } else { + transactionData.setNonce(-1); + } + + transaction.sign(alice); + + assertEquals(isValid, transaction.isSignatureValid()); + + return transaction; + } + + private MessageTransaction testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { TestAccount alice = Common.getTestAccount(repository, "alice"); @@ -244,6 +393,8 @@ public class MessageTests extends Common { TransactionUtils.signAndMint(repository, transactionData, alice); BlockUtils.orphanLastBlock(repository); + + return new MessageTransaction(repository, transactionData); } } diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java index 47c68b25..9ac73166 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java @@ -11,6 +11,7 @@ import org.qortal.arbitrary.ArbitraryDataReader; import org.qortal.arbitrary.exception.MissingDataException; import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Service; +import org.qortal.block.BlockChain; import org.qortal.controller.arbitrary.ArbitraryDataManager; import org.qortal.data.arbitrary.ArbitraryResourceMetadata; import org.qortal.data.transaction.ArbitraryTransactionData; @@ -24,6 +25,7 @@ import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.transaction.TestTransaction; import org.qortal.transaction.RegisterNameTransaction; import org.qortal.utils.Base58; +import org.qortal.utils.NTP; import java.io.IOException; import java.nio.file.Files; @@ -106,8 +108,9 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, title, description, tags, category); // Check the chunk count is correct @@ -157,8 +160,9 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, title, description, tags, category); // Check the chunk count is correct @@ -220,8 +224,9 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, title, description, tags, category); // Check the chunk count is correct @@ -272,8 +277,9 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, title, description, tags, category); // Check the chunk count is correct @@ -316,8 +322,9 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, title, description, tags, category); // Check the metadata is correct diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionTests.java index 855aeafd..089f0475 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionTests.java @@ -10,6 +10,7 @@ import org.qortal.arbitrary.ArbitraryDataTransactionBuilder; import org.qortal.arbitrary.exception.MissingDataException; import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Service; +import org.qortal.block.BlockChain; import org.qortal.controller.arbitrary.ArbitraryDataManager; import org.qortal.crypto.Crypto; import org.qortal.data.PaymentData; @@ -50,51 +51,6 @@ public class ArbitraryTransactionTests extends Common { Common.useDefaultSettings(); } - @Test - public void testDifficultyTooLow() throws IllegalAccessException, DataException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - String publicKey58 = Base58.encode(alice.getPublicKey()); - String name = "TEST"; // Can be anything for this test - String identifier = null; // Not used for this test - Service service = Service.ARBITRARY_DATA; - int chunkSize = 100; - int dataLength = 900; // Actual data length will be longer due to encryption - - // Register the name to Alice - RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""); - registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp())); - TransactionUtils.signAndMint(repository, registerNameTransactionData, alice); - - // Set difficulty to 1 - FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true); - - // Create PUT transaction - Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); - ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize); - - // Check that nonce validation succeeds - byte[] signature = arbitraryDataFile.getSignature(); - TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); - ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData); - assertTrue(transaction.isSignatureValid()); - - // Increase difficulty to 15 - FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true); - - // Make sure the nonce validation fails - // Note: there is a very tiny chance this could succeed due to being extremely lucky - // and finding a high difficulty nonce in the first couple of cycles. It will be rare - // enough that we shouldn't need to account for it. - assertFalse(transaction.isSignatureValid()); - - // Reduce difficulty back to 1, to double check - FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true); - assertTrue(transaction.isSignatureValid()); - - } - } - @Test public void testNonceAndFee() throws IllegalAccessException, DataException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { @@ -497,8 +453,9 @@ public class ArbitraryTransactionTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, null, null, null, null); byte[] signature = arbitraryDataFile.getSignature(); @@ -556,8 +513,9 @@ public class ArbitraryTransactionTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, title, description, tags, category); byte[] signature = arbitraryDataFile.getSignature(); @@ -614,8 +572,9 @@ public class ArbitraryTransactionTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true); + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false, null, null, null, null); byte[] signature = arbitraryDataFile.getSignature(); diff --git a/src/test/java/org/qortal/test/common/ArbitraryUtils.java b/src/test/java/org/qortal/test/common/ArbitraryUtils.java index 1741d22c..e08eb0ac 100644 --- a/src/test/java/org/qortal/test/common/ArbitraryUtils.java +++ b/src/test/java/org/qortal/test/common/ArbitraryUtils.java @@ -5,10 +5,12 @@ import org.qortal.arbitrary.ArbitraryDataFile; import org.qortal.arbitrary.ArbitraryDataTransactionBuilder; import org.qortal.arbitrary.misc.Category; import org.qortal.arbitrary.misc.Service; +import org.qortal.block.BlockChain; import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.transaction.Transaction; +import org.qortal.utils.NTP; import java.io.BufferedWriter; import java.io.File; @@ -20,16 +22,15 @@ import java.nio.file.Paths; import java.util.List; import java.util.Random; -import static org.junit.Assert.assertEquals; - public class ArbitraryUtils { public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier, ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account, int chunkSize) throws DataException { + long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime()); return ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, method, service, - account, chunkSize, 0L, true, null, null, null, null); + account, chunkSize, fee, false, null, null, null, null); } public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier, @@ -47,7 +48,9 @@ public class ArbitraryUtils { } ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData(); Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, account); - assertEquals(Transaction.ValidationResult.OK, result); + if (result != Transaction.ValidationResult.OK) { + throw new DataException(String.format("Arbitrary transaction invalid: %s", result.toString())); + } BlockUtils.mintBlock(repository); // We need a new ArbitraryDataFile instance because the files will have been moved to the signature's folder diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java index 91eb3dc9..401b03b9 100644 --- a/src/test/java/org/qortal/test/naming/MiscTests.java +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -444,6 +444,7 @@ public class MiscTests extends Common { // Payment PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe"); PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000); transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp())); @@ -473,16 +474,16 @@ public class MiscTests extends Common { Transaction transaction = Transaction.fromData(repository, transactionData); transaction.sign(alice); ValidationResult result = transaction.importAsUnconfirmed(); - assertTrue("Transaction should be valid", ValidationResult.OK == result); + assertEquals("Transaction should be valid", ValidationResult.OK, result); // Now try fetching and setting fee manually - transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 50000); + transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), chloe.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); + assertEquals("Transaction should be valid", ValidationResult.OK, result); } } diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 25915e9a..7059e035 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -18,6 +18,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index ae499491..1016bc17 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -22,6 +22,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index e6b327b1..5f29bc97 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 9fda9b1f..86f2def1 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 4ff3ec3c..b2da6489 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 306fd9a3..2933a63d 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, 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 6128baa7..40e40673 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 9b6bccda..8ceafe63 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 1ffa8baf..68a79ed4 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 6c3b3276..cc02a73e 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 09627e1f..5c508188 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -22,6 +22,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, 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 81cda1e8..244d2491 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 2e96e911..9168a0de 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -24,6 +24,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 },