From 3739920ad38d4750553be2c54a8b25de7588cabd Mon Sep 17 00:00:00 2001 From: CalDescent Date: Mon, 6 Mar 2023 13:17:48 +0000 Subject: [PATCH] Added support for an optional fee in arbitrary transactions, to give the option for data to be published instantly (i.e. no proof of work / mempow required when fee is sufficient). Takes effect at a future undecided timestamp. --- .../api/resource/ArbitraryResource.java | 35 +- .../ArbitraryDataTransactionBuilder.java | 6 +- .../java/org/qortal/block/BlockChain.java | 7 +- .../transaction/ArbitraryTransaction.java | 18 +- src/main/resources/blockchain.json | 3 +- .../ArbitraryDataStoragePolicyTests.java | 2 +- .../ArbitraryTransactionMetadataTests.java | 8 +- .../arbitrary/ArbitraryTransactionTests.java | 346 +++++++++++++++++- .../qortal/test/common/ArbitraryUtils.java | 11 +- .../test-chain-v2-block-timestamps.json | 3 +- .../test-chain-v2-disable-reference.json | 3 +- .../test-chain-v2-founder-rewards.json | 3 +- .../test-chain-v2-leftover-reward.json | 3 +- src/test/resources/test-chain-v2-minting.json | 5 +- .../test-chain-v2-qora-holder-extremes.json | 3 +- .../test-chain-v2-qora-holder-reduction.json | 3 +- .../resources/test-chain-v2-qora-holder.json | 3 +- .../test-chain-v2-reward-levels.json | 3 +- .../test-chain-v2-reward-scaling.json | 3 +- .../test-chain-v2-reward-shares.json | 3 +- .../test-chain-v2-self-sponsorship-algo.json | 3 +- src/test/resources/test-chain-v2.json | 3 +- 22 files changed, 433 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 0df81d9b..235e3edc 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -773,6 +773,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String path) { Security.checkApiCallAllowed(request); @@ -781,7 +782,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false, - title, description, tags, category); + fee, title, description, tags, category); } @POST @@ -818,6 +819,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String path) { Security.checkApiCallAllowed(request); @@ -826,7 +828,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, identifier, path, null, null, false, - title, description, tags, category); + fee, title, description, tags, category); } @@ -864,6 +866,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String base64) { Security.checkApiCallAllowed(request); @@ -872,7 +875,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false, - title, description, tags, category); + fee, title, description, tags, category); } @POST @@ -907,6 +910,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String base64) { Security.checkApiCallAllowed(request); @@ -915,7 +919,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64, false, - title, description, tags, category); + fee, title, description, tags, category); } @@ -952,6 +956,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String base64Zip) { Security.checkApiCallAllowed(request); @@ -960,7 +965,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true, - title, description, tags, category); + fee, title, description, tags, category); } @POST @@ -995,6 +1000,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String base64Zip) { Security.checkApiCallAllowed(request); @@ -1003,7 +1009,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64Zip, true, - title, description, tags, category); + fee, title, description, tags, category); } @@ -1043,6 +1049,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String string) { Security.checkApiCallAllowed(request); @@ -1051,7 +1058,7 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false, - title, description, tags, category); + fee, title, description, tags, category); } @POST @@ -1088,6 +1095,7 @@ public class ArbitraryResource { @QueryParam("description") String description, @QueryParam("tags") List tags, @QueryParam("category") Category category, + @QueryParam("fee") Long fee, String string) { Security.checkApiCallAllowed(request); @@ -1096,14 +1104,14 @@ public class ArbitraryResource { } return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false, - title, description, tags, category); + fee, title, description, tags, category); } // Shared methods - private String upload(Service service, String name, String identifier, - String path, String string, String base64, boolean zipped, + private String upload(Service service, String name, String identifier, String path, + String string, String base64, boolean zipped, Long fee, String title, String description, List tags, Category category) { // Fetch public key from registered name try (final Repository repository = RepositoryManager.getRepository()) { @@ -1167,9 +1175,14 @@ public class ArbitraryResource { } } + // Default to zero fee if not specified + if (fee == null) { + fee = 0L; + } + try { ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder( - repository, publicKey58, Paths.get(path), name, null, service, identifier, + repository, publicKey58, fee, Paths.get(path), name, null, service, identifier, title, description, tags, category ); diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java index 0f3d4357..b27e511c 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataTransactionBuilder.java @@ -46,6 +46,7 @@ public class ArbitraryDataTransactionBuilder { private static final double MAX_FILE_DIFF = 0.5f; private final String publicKey58; + private final long fee; private final Path path; private final String name; private Method method; @@ -64,11 +65,12 @@ public class ArbitraryDataTransactionBuilder { private ArbitraryTransactionData arbitraryTransactionData; private ArbitraryDataFile arbitraryDataFile; - public ArbitraryDataTransactionBuilder(Repository repository, String publicKey58, Path path, String name, + public ArbitraryDataTransactionBuilder(Repository repository, String publicKey58, long fee, Path path, String name, Method method, Service service, String identifier, String title, String description, List tags, Category category) { this.repository = repository; this.publicKey58 = publicKey58; + this.fee = fee; this.path = path; this.name = name; this.method = method; @@ -261,7 +263,7 @@ public class ArbitraryDataTransactionBuilder { } final BaseTransactionData baseTransactionData = new BaseTransactionData(now, Group.NO_GROUP, - lastReference, creatorPublicKey, 0L, null); + lastReference, creatorPublicKey, fee, null); final int size = (int) arbitraryDataFile.size(); final int version = 5; final int nonce = 0; diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index b96350e6..88880887 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -78,7 +78,8 @@ public class BlockChain { onlineAccountMinterLevelValidationHeight, selfSponsorshipAlgoV1Height, feeValidationFixTimestamp, - chatReferenceTimestamp; + chatReferenceTimestamp, + arbitraryOptionalFeeTimestamp; } // Custom transaction fees @@ -522,6 +523,10 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue(); } + public long getArbitraryOptionalFeeTimestamp() { + return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue(); + } + // More complex getters for aspects that change by height or timestamp diff --git a/src/main/java/org/qortal/transaction/ArbitraryTransaction.java b/src/main/java/org/qortal/transaction/ArbitraryTransaction.java index 7e7d4040..3452f916 100644 --- a/src/main/java/org/qortal/transaction/ArbitraryTransaction.java +++ b/src/main/java/org/qortal/transaction/ArbitraryTransaction.java @@ -88,6 +88,12 @@ 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 + if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) { + return super.isFeeValid(); + } + return ValidationResult.OK; } @@ -208,10 +214,14 @@ public class ArbitraryTransaction extends Transaction { // Clear nonce from transactionBytes ArbitraryTransactionTransformer.clearNonce(transactionBytes); - // We only need to check nonce for recent transactions due to PoW verification overhead - if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) { - int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty(); - return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce); + // As of feature-trigger timestamp, we only require a nonce when the fee is 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 + if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) { + int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty(); + return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce); + } } } diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 46b4b4f9..7ce93a28 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -85,7 +85,8 @@ "onlineAccountMinterLevelValidationHeight": 1092000, "selfSponsorshipAlgoV1Height": 1092400, "feeValidationFixTimestamp": 1671918000000, - "chatReferenceTimestamp": 1674316800000 + "chatReferenceTimestamp": 1674316800000, + "arbitraryOptionalFeeTimestamp": 9999999999999 }, "checkpoints": [ { "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" } diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryDataStoragePolicyTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryDataStoragePolicyTests.java index 9bf76127..49e645cf 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryDataStoragePolicyTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryDataStoragePolicyTests.java @@ -246,7 +246,7 @@ public class ArbitraryDataStoragePolicyTests extends Common { Path path = Paths.get("src/test/resources/arbitrary/demo1"); ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder( - repository, publicKey58, path, name, Method.PUT, Service.ARBITRARY_DATA, null, + repository, publicKey58, 0L, path, name, Method.PUT, Service.ARBITRARY_DATA, null, null, null, null, null); txnBuilder.build(); diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java index 5d28568d..bf4f0a70 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java @@ -107,7 +107,7 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, title, description, tags, category); // Check the chunk count is correct @@ -157,7 +157,7 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, title, description, tags, category); // Check the chunk count is correct @@ -219,7 +219,7 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, title, description, tags, category); // Check the chunk count is correct @@ -273,7 +273,7 @@ public class ArbitraryTransactionMetadataTests extends Common { // Create PUT transaction Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, - identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, 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 294e463e..2c2d52b2 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionTests.java @@ -5,6 +5,7 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.arbitrary.ArbitraryDataFile; +import org.qortal.arbitrary.ArbitraryDataTransactionBuilder; import org.qortal.arbitrary.exception.MissingDataException; import org.qortal.arbitrary.misc.Service; import org.qortal.controller.arbitrary.ArbitraryDataManager; @@ -20,9 +21,11 @@ import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.transaction.TestTransaction; import org.qortal.transaction.ArbitraryTransaction; import org.qortal.transaction.RegisterNameTransaction; +import org.qortal.transaction.Transaction; import org.qortal.utils.Base58; import org.qortal.utils.NTP; +import javax.xml.crypto.Data; import java.io.IOException; import java.nio.file.Path; @@ -36,7 +39,7 @@ public class ArbitraryTransactionTests extends Common { } @Test - public void testDifficultyTooLow() throws IllegalAccessException, DataException, IOException, MissingDataException { + 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()); @@ -78,7 +81,346 @@ public class ArbitraryTransactionTests extends Common { assertTrue(transaction.isSignatureValid()); } - } + @Test + public void testNonceAndFee() 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, with a fee + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 10000000; // sufficient + boolean computeNonce = true; + ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null); + + // 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 that nonce validation still succeeds, as the fee has allowed us to avoid including a nonce + assertTrue(transaction.isSignatureValid()); + } + } + + @Test + public void testNonceAndLowFee() 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, with a fee that is too low + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 9999999; // insufficient + boolean computeNonce = true; + boolean insufficientFeeDetected = false; + try { + ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null); + } + catch (DataException e) { + if (e.getMessage().contains("INSUFFICIENT_FEE")) { + insufficientFeeDetected = true; + } + } + + // Transaction should be invalid due to an insufficient fee + assertTrue(insufficientFeeDetected); + } + } + + @Test + public void testFeeNoNonce() 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, with a fee + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 10000000; // sufficient + boolean computeNonce = false; + ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null); + + // Check that nonce validation succeeds, even though it wasn't computed. This is because we have included a sufficient fee. + 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 that nonce validation still succeeds, as the fee has allowed us to avoid including a nonce + assertTrue(transaction.isSignatureValid()); + } + } + + @Test + public void testLowFeeNoNonce() 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, with a fee that is too low. Also, don't compute a nonce. + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 9999999; // insufficient + + ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder( + repository, publicKey58, fee, path1, name, ArbitraryTransactionData.Method.PUT, service, identifier, null, null, null, null); + + txnBuilder.setChunkSize(chunkSize); + txnBuilder.build(); + ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData(); + Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, alice); + + // Transaction should be invalid due to an insufficient fee + assertEquals(Transaction.ValidationResult.INSUFFICIENT_FEE, result); + } + } + + @Test + public void testZeroFeeNoNonce() 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, with a fee that is too low. Also, don't compute a nonce. + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 0L; + + ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder( + repository, publicKey58, fee, path1, name, ArbitraryTransactionData.Method.PUT, service, identifier, null, null, null, null); + + txnBuilder.setChunkSize(chunkSize); + txnBuilder.build(); + ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData(); + ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData); + + // Transaction should be invalid + assertFalse(arbitraryTransaction.isSignatureValid()); + } + } + + @Test + public void testNonceAndFeeBeforeFeatureTrigger() throws IllegalAccessException, DataException, IOException { + // Use v2-minting settings, as these are pre-feature-trigger + Common.useSettings("test-settings-v2-minting.json"); + + 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, with a fee + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 10000000; // sufficient + boolean computeNonce = true; + ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null); + + // 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, as we aren't allowing a fee to replace a nonce yet. + // 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 testNonceAndInsufficientFeeBeforeFeatureTrigger() throws IllegalAccessException, DataException, IOException { + // Use v2-minting settings, as these are pre-feature-trigger + Common.useSettings("test-settings-v2-minting.json"); + + 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, with a fee + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 9999999; // insufficient + boolean computeNonce = true; + ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null); + + // Check that nonce validation succeeds + byte[] signature = arbitraryDataFile.getSignature(); + TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); + ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData); + assertTrue(transaction.isSignatureValid()); + + // The transaction should be valid because we don't care about the fee (before the feature trigger) + assertEquals(Transaction.ValidationResult.OK, transaction.isValidUnconfirmed()); + + // Increase difficulty to 15 + FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true); + + // Make sure the nonce validation fails, as we aren't allowing a fee to replace a nonce yet (and it was insufficient anyway) + // 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 testNonceAndZeroFeeBeforeFeatureTrigger() throws IllegalAccessException, DataException, IOException { + // Use v2-minting settings, as these are pre-feature-trigger + Common.useSettings("test-settings-v2-minting.json"); + + 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, with a fee + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + long fee = 0L; + boolean computeNonce = true; + ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null); + + // Check that nonce validation succeeds + byte[] signature = arbitraryDataFile.getSignature(); + TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); + ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData); + assertTrue(transaction.isSignatureValid()); + + // The transaction should be valid because we don't care about the fee (before the feature trigger) + assertEquals(Transaction.ValidationResult.OK, transaction.isValidUnconfirmed()); + + // Increase difficulty to 15 + FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true); + + // Make sure the nonce validation fails, as we aren't allowing a fee to replace a nonce yet (and it was insufficient anyway) + // 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()); + } + } } diff --git a/src/test/java/org/qortal/test/common/ArbitraryUtils.java b/src/test/java/org/qortal/test/common/ArbitraryUtils.java index 81abf47f..73dc8097 100644 --- a/src/test/java/org/qortal/test/common/ArbitraryUtils.java +++ b/src/test/java/org/qortal/test/common/ArbitraryUtils.java @@ -29,19 +29,22 @@ public class ArbitraryUtils { int chunkSize) throws DataException { return ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, method, service, - account, chunkSize, null, null, null, null); + account, chunkSize, 0L, true, null, null, null, null); } public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier, ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account, - int chunkSize, String title, String description, List tags, Category category) throws DataException { + int chunkSize, long fee, boolean computeNonce, + String title, String description, List tags, Category category) throws DataException { ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder( - repository, publicKey58, path, name, method, service, identifier, title, description, tags, category); + repository, publicKey58, fee, path, name, method, service, identifier, title, description, tags, category); txnBuilder.setChunkSize(chunkSize); txnBuilder.build(); - txnBuilder.computeNonce(); + if (computeNonce) { + txnBuilder.computeNonce(); + } ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData(); Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, account); assertEquals(Transaction.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 8c2e0503..3b4de702 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -75,7 +75,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index f7f8e7d8..c93fbb78 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -78,7 +78,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 20d10233..1b068932 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index e71ebab6..aef76cc2 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 2a388e1f..db6d8a0b 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -74,12 +74,13 @@ "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, - "disableReferenceTimestamp": 9999999999999, + "disableReferenceTimestamp": 0, "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 9999999999999 }, "genesisInfo": { "version": 4, 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 cface0e7..2452d4d2 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, 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 f233680b..23193729 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -80,7 +80,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 4ea82290..9d81632b 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 5de8d9ff..81609595 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index c008ed42..21a5b7a7 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 2fc0151f..6119ac48 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, 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 68b33cc3..dc5f3961 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 20, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 63abc695..d0c460df 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -79,7 +79,8 @@ "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, - "chatReferenceTimestamp": 0 + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4,