diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 41faf51b..af99e34e 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1309,6 +1309,9 @@ public class Block { if (!transaction.isConfirmable()) { return ValidationResult.TRANSACTION_NOT_CONFIRMABLE; } + if (!transaction.isConfirmableAtHeight(this.blockData.getHeight())) { + return ValidationResult.TRANSACTION_NOT_CONFIRMABLE; + } } // Check transaction isn't already included in a block @@ -2088,7 +2091,7 @@ public class Block { return Block.isOnlineAccountsBlock(this.getBlockData().getHeight()); } - private static boolean isOnlineAccountsBlock(int height) { + public static boolean isOnlineAccountsBlock(int height) { // After feature trigger, only certain blocks contain online accounts if (height >= BlockChain.getInstance().getBlockRewardBatchStartHeight()) { final int leadingBlockCount = BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount(); diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index aa2ab9bb..8013e7a5 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -75,7 +75,8 @@ public class BlockChain { selfSponsorshipAlgoV1Height, feeValidationFixTimestamp, chatReferenceTimestamp, - arbitraryOptionalFeeTimestamp; + arbitraryOptionalFeeTimestamp, + unconfirmableRewardSharesHeight; } // Custom transaction fees @@ -556,6 +557,10 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue(); } + public int getUnconfirmableRewardSharesHeight() { + return this.featureTriggers.get(FeatureTrigger.unconfirmableRewardSharesHeight.name()).intValue(); + } + // More complex getters for aspects that change by height or timestamp diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 15bcb1d7..49831cba 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -474,6 +474,7 @@ public class BlockMinter extends Thread { Iterator unconfirmedTransactionsIterator = unconfirmedTransactions.iterator(); final long newBlockTimestamp = newBlock.getBlockData().getTimestamp(); + final int newBlockHeight = newBlock.getBlockData().getHeight(); while (unconfirmedTransactionsIterator.hasNext()) { TransactionData transactionData = unconfirmedTransactionsIterator.next(); @@ -481,6 +482,12 @@ public class BlockMinter extends Thread { // Ignore transactions that have expired before this block - they will be cleaned up later if (transactionData.getTimestamp() > newBlockTimestamp || Transaction.getDeadline(transactionData) <= newBlockTimestamp) unconfirmedTransactionsIterator.remove(); + + // Ignore transactions that are unconfirmable at this block height + Transaction transaction = Transaction.fromData(repository, transactionData); + if (!transaction.isConfirmableAtHeight(newBlockHeight)) { + unconfirmedTransactionsIterator.remove(); + } } // Sign to create block's signature, needed by Block.isValid() diff --git a/src/main/java/org/qortal/transaction/RewardShareTransaction.java b/src/main/java/org/qortal/transaction/RewardShareTransaction.java index ab66dec6..1b608a0c 100644 --- a/src/main/java/org/qortal/transaction/RewardShareTransaction.java +++ b/src/main/java/org/qortal/transaction/RewardShareTransaction.java @@ -3,6 +3,7 @@ package org.qortal.transaction; import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.account.RewardShareData; @@ -180,6 +181,17 @@ public class RewardShareTransaction extends Transaction { // Nothing to do } + @Override + public boolean isConfirmableAtHeight(int height) { + if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) { + // Not confirmable in online accounts or distribution blocks + if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) { + return false; + } + } + return true; + } + @Override public void process() throws DataException { PublicKeyAccount mintingAccount = getMintingAccount(); diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index 61b78ade..0b4f6e90 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -904,6 +904,15 @@ public abstract class Transaction { return true; } + /** + * Returns whether transaction is confirmable in a block at a given height. + * @return + */ + public boolean isConfirmableAtHeight(int height) { + /* 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 2fc69347..23e43285 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -95,7 +95,8 @@ "selfSponsorshipAlgoV1Height": 1092400, "feeValidationFixTimestamp": 1671918000000, "chatReferenceTimestamp": 1674316800000, - "arbitraryOptionalFeeTimestamp": 1680278400000 + "arbitraryOptionalFeeTimestamp": 1680278400000, + "unconfirmableRewardSharesHeight": 99999500 }, "checkpoints": [ { "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" } diff --git a/src/test/java/org/qortal/test/minting/BatchRewardTests.java b/src/test/java/org/qortal/test/minting/BatchRewardTests.java index bd89384b..ad8d8d66 100644 --- a/src/test/java/org/qortal/test/minting/BatchRewardTests.java +++ b/src/test/java/org/qortal/test/minting/BatchRewardTests.java @@ -20,6 +20,7 @@ import org.qortal.settings.Settings; import org.qortal.test.common.*; import org.qortal.test.common.transaction.TestTransaction; import org.qortal.transaction.DeployAtTransaction; +import org.qortal.transaction.Transaction; import org.qortal.transform.TransformationException; import org.qortal.transform.block.BlockTransformer; import org.qortal.utils.NTP; @@ -679,4 +680,120 @@ public class BatchRewardTests extends Common { } } + @Test + public void testUnconfirmableRewardShares() throws DataException, IllegalAccessException { + // test-settings-v2-reward-scaling.json has unconfirmable reward share feature trigger enabled from block 500 + Common.useSettings("test-settings-v2-reward-scaling.json"); + + // Set reward batching to every 1000 blocks, starting at block 0, looking back the last 25 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 1000, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 25, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert"); + + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share"); + PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share"); + + // Create self shares for bob, chloe and dilbert + AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert)); + + // Mint blocks 1-974 - these should have no online accounts or rewards + for (int i=1; i<974; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 2); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertFalse(block.isOnlineAccountsBlock()); + assertEquals(0, block.getBlockData().getOnlineAccountsCount()); + } + + // Mint blocks 975-998 - these should have online accounts but no rewards + for (int i=975; i<=998; i++) { + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertTrue(block.isOnlineAccountsBlock()); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + } + + // Cancel Chloe's reward share + TransactionData transactionData = AccountUtils.createRewardShare(repository, chloe, chloe, -100, 10000000L); + TransactionUtils.signAndImportValid(repository, transactionData, chloe); + + // Mint block 999 - Chloe's account should still be included as the reward share cancellation is delayed + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertTrue(block.isOnlineAccountsBlock()); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + + // Mint block 1000 + Block block1000 = BlockUtils.mintBlockWithReorgs(repository, 12); + + // Online accounts should be included from block 999 + assertEquals(3, block1000.getBlockData().getOnlineAccountsCount()); + + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 1000); + + // It's a distribution block (which is technically also an online accounts block) + assertTrue(block1000.isBatchRewardDistributionBlock()); + assertTrue(block1000.isRewardDistributionBlock()); + assertTrue(block1000.isBatchRewardDistributionActive()); + assertTrue(block1000.isOnlineAccountsBlock()); + } + } + + @Test + public void testUnconfirmableRewardShareBlocks() throws DataException, IllegalAccessException { + // test-settings-v2-reward-scaling.json has unconfirmable reward share feature trigger enabled from block 500 + Common.useSettings("test-settings-v2-reward-scaling.json"); + + // Set reward batching to every 1000 blocks, starting at block 0, looking back the last 25 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 1000, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 25, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert"); + + // Create self shares for bob, chloe and dilbert + AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert)); + + // Create transaction to cancel chloe's reward share + TransactionData rewardShareTransactionData = AccountUtils.createRewardShare(repository, chloe, chloe, -100, 10000000L); + Transaction rewardShareTransaction = Transaction.fromData(repository, rewardShareTransactionData); + + // Mint a block + BlockUtils.mintBlock(repository); + + // Check block heights up to 974 - transaction should be confirmable + for (int height=2; height<974; height++) { + assertEquals(true, rewardShareTransaction.isConfirmableAtHeight(height)); + } + + // Check block heights 975-1000 - transaction should not be confirmable + for (int height=975; height<1000; height++) { + assertEquals(false, rewardShareTransaction.isConfirmableAtHeight(height)); + } + + // Check block heights 1001-1974 - transaction should be confirmable again + for (int height=1001; height<1974; height++) { + assertEquals(true, rewardShareTransaction.isConfirmableAtHeight(height)); + } + } + } + } diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 2d3a6484..e5460bec 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -83,7 +83,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 30691293..3d76620a 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -86,7 +86,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 9b273323..e778f792 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 e7339947..ec64f5a9 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index fe03c37d..72051c29 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 9999999999999 + "arbitraryOptionalFeeTimestamp": 9999999999999, + "unconfirmableRewardSharesHeight": 99999999 }, "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 8acc0a35..4b8d4b80 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 642c6415..5484745b 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -88,7 +88,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 aa1a23f3..50b4910a 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 3073dfa9..99c98e2f 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 d602de18..1c59476a 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 500 }, "genesisInfo": { "version": 4, @@ -107,7 +108,10 @@ { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 }, - { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 } + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }, + { "type": "ACCOUNT_LEVEL", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 1 } ] } } diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 1261be0d..fd63add0 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "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 a63a395f..04e81e21 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo.json @@ -87,7 +87,8 @@ "selfSponsorshipAlgoV1Height": 20, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 2a7aa362..a57a0fd4 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -88,7 +88,8 @@ "selfSponsorshipAlgoV1Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999 }, "genesisInfo": { "version": 4,