From 8b69b65712ddd994736cb90018afa987ea8a93c5 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 17 Sep 2023 15:04:37 +0100 Subject: [PATCH 1/3] Initial implementation of batch block reward distributions. There are 3 values that need to be specified for this feature trigger: - blockRewardBatchSize: the number of blocks per batch (1000 recommended). - blockRewardBatchStartHeight: the block height at which the system switches to batch reward distributions. Must be a multiple of blockRewardBatchSize. Ideally this would be set to a block height that will occur at least 1-2 weeks after the release / auto update goes live. - blockRewardBatchAccountsBlockCount: the number of blocks to include online accounts in the lead up to the batch reward distribution block (25 recommended). Once active, rewards are no longer distributed every block and instead are distributed every 1000th block. This includes all fees from the distribution block and the previous 999 blocks. The online accounts used for the distribution are taken from one of the previous 25 blocks (e.g. blocks xxxx975-xxxx999). The validation ensures that it is always the block with the highest number of online accounts in this range. If this number of online accounts is shared by multiple blocks, it will pick the one with the lowest height. The idea behind 25 blocks is that it's low enough to reduce load in the other 975 blocks, but high enough for it to be extremely difficult for block signers to influence reward payouts. Batch distribution blocks contain a copy of the online accounts from one of these 25 preceding blocks. However, online account signatures and online accounts timestamp are excluded to save space. The core will validate that the copy of online accounts is exactly matching those of the earlier validated block, and are therefore valid. Fairly comprehensive unit tests have been written but this needs a lot more testing on testnets before it can be considered stable. Future note: Once released, the online accounts mempow can optionally be scaled back so that it is no longer running 24/7, and instead is only running for 2-3 hours each day in the lead up to the batch distribution block. In each 1000 block cycle, the mempow would ideally start at least an hour before block xxxx975 is due to be minted, and continue for an hour after block xxxx000 (the reward distribution block). None of the ideas mentioned in this last paragraph are coded yet. --- src/main/java/org/qortal/block/Block.java | 382 ++++++++-- .../java/org/qortal/block/BlockChain.java | 44 ++ .../org/qortal/controller/BlockMinter.java | 18 +- .../controller/OnlineAccountsManager.java | 7 + .../java/org/qortal/data/block/BlockData.java | 8 + .../qortal/repository/BlockRepository.java | 11 + .../hsqldb/HSQLDBBlockRepository.java | 30 + .../java/org/qortal/settings/Settings.java | 4 +- .../transform/block/BlockTransformer.java | 4 + src/main/resources/blockchain.json | 3 + .../org/qortal/test/common/AccountUtils.java | 27 + .../org/qortal/test/common/BlockUtils.java | 35 + .../qortal/test/minting/BatchRewardTests.java | 682 ++++++++++++++++++ .../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 | 3 + .../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 + 26 files changed, 1209 insertions(+), 85 deletions(-) create mode 100644 src/test/java/org/qortal/test/minting/BatchRewardTests.java diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 16c061da..9d40d8da 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -376,83 +376,107 @@ public class Block { int height = parentBlockData.getHeight() + 1; long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel); - long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp(); - // Fetch our list of online accounts, removing any that are missing a nonce - List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp); - onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0); + Long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp(); + byte[] encodedOnlineAccounts = new byte[0]; + int onlineAccountsCount = 0; + byte[] onlineAccountsSignatures = null; + + if (isBatchRewardDistributionBlock(height)) { + // Batch reward distribution block - copy online accounts from recent block with highest online accounts count - // After feature trigger, remove any online accounts that are level 0 - if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) { - onlineAccounts.removeIf(a -> { - try { - return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0; - } catch (DataException e) { - // Something went wrong, so remove the account - return true; - } - }); + int firstBlock = height - BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount(); + int lastBlock = height - 1; + BlockData highOnlineAccountsBlock = repository.getBlockRepository().getBlockInRangeWithHighestOnlineAccountsCount(firstBlock, lastBlock); + encodedOnlineAccounts = highOnlineAccountsBlock.getEncodedOnlineAccounts(); + onlineAccountsCount = highOnlineAccountsBlock.getOnlineAccountsCount(); + // No point in copying signatures since these aren't revalidated, and because of this onlineAccountsTimestamp must be null too + onlineAccountsSignatures = null; + onlineAccountsTimestamp = null; } + else if (isOnlineAccountsBlock(height)) { + // Standard online accounts block - add online accounts in regular way - if (onlineAccounts.isEmpty()) { - LOGGER.debug("No online accounts - not even our own?"); - return null; - } + // Fetch our list of online accounts, removing any that are missing a nonce + List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp); + onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0); - // Load sorted list of reward share public keys into memory, so that the indexes can be obtained. - // This is up to 100x faster than querying each index separately. For 4150 reward share keys, it - // was taking around 5000ms to query individually, vs 50ms using this approach. - List allRewardSharePublicKeys = repository.getAccountRepository().getRewardSharePublicKeys(); - - // Map using index into sorted list of reward-shares as key - Map indexedOnlineAccounts = new HashMap<>(); - for (OnlineAccountData onlineAccountData : onlineAccounts) { - Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys); - if (accountIndex == null) - // Online account (reward-share) with current timestamp but reward-share cancelled - continue; - - indexedOnlineAccounts.put(accountIndex, onlineAccountData); - } - List accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet()); - accountIndexes.sort(null); - - // Convert to compressed integer set - ConciseSet onlineAccountsSet = new ConciseSet(); - onlineAccountsSet = onlineAccountsSet.convert(accountIndexes); - byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet); - int onlineAccountsCount = onlineAccountsSet.size(); - - // Collate all signatures - Collection signaturesToAggregate = indexedOnlineAccounts.values() - .stream() - .map(OnlineAccountData::getSignature) - .collect(Collectors.toList()); - - // Aggregated, single signature - byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate); - - // Add nonces to the end of the online accounts signatures - try { - // Create ordered list of nonce values - List nonces = new ArrayList<>(); - for (int i = 0; i < onlineAccountsCount; ++i) { - Integer accountIndex = accountIndexes.get(i); - OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex); - nonces.add(onlineAccountData.getNonce()); + // After feature trigger, remove any online accounts that are level 0 + if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) { + onlineAccounts.removeIf(a -> { + try { + return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0; + } catch (DataException e) { + // Something went wrong, so remove the account + return true; + } + }); } - // Encode the nonces to a byte array - byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces); + if (onlineAccounts.isEmpty()) { + LOGGER.debug("No online accounts - not even our own?"); + return null; + } + + // Load sorted list of reward share public keys into memory, so that the indexes can be obtained. + // This is up to 100x faster than querying each index separately. For 4150 reward share keys, it + // was taking around 5000ms to query individually, vs 50ms using this approach. + List allRewardSharePublicKeys = repository.getAccountRepository().getRewardSharePublicKeys(); + + // Map using index into sorted list of reward-shares as key + Map indexedOnlineAccounts = new HashMap<>(); + for (OnlineAccountData onlineAccountData : onlineAccounts) { + Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys); + if (accountIndex == null) + // Online account (reward-share) with current timestamp but reward-share cancelled + continue; + + indexedOnlineAccounts.put(accountIndex, onlineAccountData); + } + List accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet()); + accountIndexes.sort(null); + + // Convert to compressed integer set + ConciseSet onlineAccountsSet = new ConciseSet(); + onlineAccountsSet = onlineAccountsSet.convert(accountIndexes); + encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet); + onlineAccountsCount = onlineAccountsSet.size(); + + // Collate all signatures + Collection signaturesToAggregate = indexedOnlineAccounts.values() + .stream() + .map(OnlineAccountData::getSignature) + .collect(Collectors.toList()); + + // Aggregated, single signature + onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate); + + // Add nonces to the end of the online accounts signatures + try { + // Create ordered list of nonce values + List nonces = new ArrayList<>(); + for (int i = 0; i < onlineAccountsCount; ++i) { + Integer accountIndex = accountIndexes.get(i); + OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex); + nonces.add(onlineAccountData.getNonce()); + } + + // Encode the nonces to a byte array + byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces); + + // Append the encoded nonces to the encoded online account signatures + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(onlineAccountsSignatures); + outputStream.write(encodedNonces); + onlineAccountsSignatures = outputStream.toByteArray(); + } catch (TransformationException | IOException e) { + return null; + } - // Append the encoded nonces to the encoded online account signatures - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - outputStream.write(onlineAccountsSignatures); - outputStream.write(encodedNonces); - onlineAccountsSignatures = outputStream.toByteArray(); } - catch (TransformationException | IOException e) { - return null; + else { + // No online accounts should be included in this block + onlineAccountsTimestamp = null; } byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData, @@ -1064,6 +1088,40 @@ public class Block { if (accountIndexes.size() != this.blockData.getOnlineAccountsCount()) return ValidationResult.ONLINE_ACCOUNTS_INVALID; + // Online accounts should only be included in designated blocks; all others must be empty + if (!this.isOnlineAccountsBlock()) { + if (this.blockData.getOnlineAccountsCount() != 0 || accountIndexes.size() != 0) { + return ValidationResult.ONLINE_ACCOUNTS_INVALID; + } + // Not a designated online accounts block and account count is 0. Everything is correct so no need to validate further. + return ValidationResult.OK; + } + + // If this is a batch reward distribution block, ensure that online accounts have been copied from the correct previous block + if (this.isBatchRewardDistributionBlock()) { + int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount(); + int lastBlock = this.getBlockData().getHeight() - 1; + BlockData highOnlineAccountsBlock = repository.getBlockRepository().getBlockInRangeWithHighestOnlineAccountsCount(firstBlock, lastBlock); + + if (this.blockData.getOnlineAccountsCount() != highOnlineAccountsBlock.getOnlineAccountsCount()) { + return ValidationResult.ONLINE_ACCOUNTS_INVALID; + } + if (!Arrays.equals(this.blockData.getEncodedOnlineAccounts(), highOnlineAccountsBlock.getEncodedOnlineAccounts())) { + return ValidationResult.ONLINE_ACCOUNTS_INVALID; + } + if (this.blockData.getOnlineAccountsSignatures() != null) { + // Signatures are excluded to reduce block size + return ValidationResult.ONLINE_ACCOUNTS_INVALID; + } + if (this.blockData.getOnlineAccountsTimestamp() != null) { + // Online accounts timestamp must be null, because no signatures are included + return ValidationResult.ONLINE_ACCOUNTS_INVALID; + } + + // Online accounts have been correctly copied, and were already validated in earlier block, so consider them valid + return ValidationResult.OK; + } + List onlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(accountIndexes.toArray()); if (onlineRewardShares == null) return ValidationResult.ONLINE_ACCOUNT_UNKNOWN; @@ -1483,11 +1541,15 @@ public class Block { LOGGER.trace(() -> String.format("Processing block %d", this.blockData.getHeight())); if (this.blockData.getHeight() > 1) { - // Increase account levels - increaseAccountLevels(); - // Distribute block rewards, including transaction fees, before transactions processed - processBlockRewards(); + // Account levels and block rewards are only processed on block reward distribution blocks + if (this.isRewardDistributionBlock()) { + // Increase account levels + increaseAccountLevels(); + + // Distribute block rewards, including transaction fees, before transactions processed + processBlockRewards(); + } if (this.blockData.getHeight() == 212937) // Apply fix for block 212937 @@ -1547,9 +1609,13 @@ public class Block { } // Increase blocks minted count for all accounts + int delta = 1; + if (this.isBatchRewardDistributionActive()) { + delta = BlockChain.getInstance().getBlockRewardBatchSize(); + } // Batch update in repository - repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1); + repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +delta); // Keep track of level bumps in case we need to apply to other entries Map bumpedAccounts = new HashMap<>(); @@ -1599,8 +1665,34 @@ public class Block { protected void processBlockRewards() throws DataException { // General block reward long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight()); - // Add transaction fees + + if (this.isBatchRewardDistributionActive()) { + // Batch distribution is active - so multiply the reward by the batch size + reward *= BlockChain.getInstance().getBlockRewardBatchSize(); + + if (!this.isRewardDistributionBlock()) { + // Shouldn't ever happen, but checking here for safety + throw new DataException("Attempted to distribute a batch reward in a non-reward-distribution block"); + } + + // Add transaction fees since last distribution block + int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchSize() + 1; + int lastBlock = this.blockData.getHeight() - 1; + Long startTime = NTP.getTime(); + Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock); + LOGGER.info("[process] Fetching total fees took {} ms", (NTP.getTime()-startTime)); + if (totalFees == null) { + throw new DataException("Unable to calculate total fees for block range"); + } + reward += totalFees; + LOGGER.info("[process] Total fees for range {} - {}: {}", firstBlock, lastBlock, totalFees); + } + + // Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet) reward += this.blockData.getTotalFees(); + LOGGER.info("[process] Total fees for block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); + + LOGGER.info("[process] Block reward for height {}: {}", this.blockData.getHeight(), reward); // Nothing to reward? if (reward <= 0) @@ -1758,11 +1850,14 @@ public class Block { else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this); - // Block rewards, including transaction fees, removed after transactions undone - orphanBlockRewards(); + // Account levels and block rewards are only processed/orphaned on block reward distribution blocks + if (this.isRewardDistributionBlock()) { + // Block rewards, including transaction fees, removed after transactions undone + orphanBlockRewards(); - // Decrease account levels - decreaseAccountLevels(); + // Decrease account levels + decreaseAccountLevels(); + } } // Delete block from blockchain @@ -1838,8 +1933,32 @@ public class Block { protected void orphanBlockRewards() throws DataException { // General block reward long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight()); - // Add transaction fees + + if (this.isBatchRewardDistributionActive()) { + // Batch distribution is active - so multiply the reward by the batch size + reward *= BlockChain.getInstance().getBlockRewardBatchSize(); + + if (!this.isRewardDistributionBlock()) { + // Shouldn't ever happen, but checking here for safety + throw new DataException("Attempted to orphan batched rewards in a non-reward-distribution block"); + } + + // Add transaction fees since last distribution block + int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchSize() + 1; + int lastBlock = this.blockData.getHeight() - 1; + Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock); + if (totalFees == null) { + throw new DataException("Unable to calculate total fees for block range"); + } + reward += totalFees; + LOGGER.info("[orphan] Total fees for range {} - {}: {}", firstBlock, lastBlock, totalFees); + } + + // Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet) reward += this.blockData.getTotalFees(); + LOGGER.info("[orphan] Total fees for block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); + + LOGGER.info("[orphan] Block reward for height {}: {}", this.blockData.getHeight(), reward); // Nothing to reward? if (reward <= 0) @@ -1880,9 +1999,13 @@ public class Block { } // Decrease blocks minted count for all accounts + int delta = 1; + if (this.isBatchRewardDistributionActive()) { + delta = BlockChain.getInstance().getBlockRewardBatchSize(); + } // Batch update in repository - repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -1); + repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -delta); for (AccountData accountData : allUniqueExpandedAccounts) { // Adjust count locally (in Java) @@ -1905,6 +2028,105 @@ public class Block { } } + + /** + * Specifies whether the batch reward feature trigger has activated yet. + * Note that the exact block of the feature trigger activation will return false, + * because this is actually the very last block with non-batched reward distributions. + * + * @return true if active, false if batch rewards feature trigger height not reached yet. + */ + public boolean isBatchRewardDistributionActive() { + return Block.isBatchRewardDistributionActive(this.blockData.getHeight()); + } + public static boolean isBatchRewardDistributionActive(int height) { + // Once the getBlockRewardBatchStartHeight is reached, reward distributions per block must stop. + // Note the > instead of >= below, as the first batch distribution isn't until 1000 blocks *after* the + // start height. The block exactly matching the start height is not batched. + return height > BlockChain.getInstance().getBlockRewardBatchStartHeight(); + } + + + /** + * Specifies whether rewards are distributed in this block, via ANY method (batch or single). + * + * @return true if rewards are to be distributed in this block. + */ + public boolean isRewardDistributionBlock() { + return Block.isRewardDistributionBlock(this.blockData.getHeight()); + } + + public static boolean isRewardDistributionBlock(int height) { + // Up to and *including* the start height (feature trigger), the rewards are distributed in every block + if (!Block.isBatchRewardDistributionActive(height)) { + return true; + } + + // After the start height (feature trigger) the rewards are distributed in blocks that are multiples of the batch size + return height % BlockChain.getInstance().getBlockRewardBatchSize() == 0; + } + + + /** + * Specifies whether BATCH rewards are distributed in this block. + * + * @return true if a batch distribution will occur, false if a single no distribution will occur. + */ + public boolean isBatchRewardDistributionBlock() { + return Block.isBatchRewardDistributionBlock(this.blockData.getHeight()); + } + + public static boolean isBatchRewardDistributionBlock(int height) { + // Up to and *including* the start height (feature trigger), batch reward distribution isn't active yet + if (!Block.isBatchRewardDistributionActive(height)) { + return false; + } + + // After the start height (feature trigger) the rewards are distributed in blocks that are multiples of the batch size + return height % BlockChain.getInstance().getBlockRewardBatchSize() == 0; + } + + + /** + * Specifies whether online accounts are to be included in this block. + * + * @return true if online accounts should be included, false if they should be excluded. + */ + public boolean isOnlineAccountsBlock() { + return Block.isOnlineAccountsBlock(this.getBlockData().getHeight()); + } + + private 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(); + return height >= (getNextBatchDistributionBlockHeight(height) - leadingBlockCount); + } + // Before feature trigger, all blocks contain online accounts + return true; + } + + + /** + * + * @param currentHeight + * + * @return the next height of a batch reward distribution. Must only be called after the + * batch reward feature trigger has activated. It is not useful prior to this. + */ + private static int getNextBatchDistributionBlockHeight(int currentHeight) { + final int batchSize = BlockChain.getInstance().getBlockRewardBatchSize(); + if (currentHeight % batchSize == 0) { + // Already a reward distribution block + return currentHeight; + } else { + // Calculate the difference needed to reach the next distribution block + final int difference = batchSize - (currentHeight % batchSize); + return currentHeight + difference; + } + } + + private static class BlockRewardCandidate { public final String description; public long share; diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 540e6cf4..1621fd77 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -212,6 +212,19 @@ public class BlockChain { /** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */ private long mempowTransactionUpdatesTimestamp; + /** Feature trigger block height for batch block reward payouts. + * This MUST be a multiple of blockRewardBatchSize. Can't use + * featureTriggers because unit tests need to set this value via Reflection. */ + private int blockRewardBatchStartHeight; + + /** Block reward batch size. Must be (significantly) less than block prune size, + * as all blocks in the range need to be present in the repository when processing/orphaning */ + private int blockRewardBatchSize; + + /** Number of blocks prior to the batch reward distribution blocks to include online accounts + * data and to base online accounts decisions on. */ + private int blockRewardBatchAccountsBlockCount; + /** Max reward shares by block height */ public static class MaxRewardSharesByTimestamp { public long timestamp; @@ -368,6 +381,21 @@ public class BlockChain { return this.onlineAccountsModulusV2Timestamp; } + + /* Block reward batching */ + public long getBlockRewardBatchStartHeight() { + return this.blockRewardBatchStartHeight; + } + + public int getBlockRewardBatchSize() { + return this.blockRewardBatchSize; + } + + public int getBlockRewardBatchAccountsBlockCount() { + return this.blockRewardBatchAccountsBlockCount; + } + + // Self sponsorship algo public long getSelfSponsorshipAlgoV1SnapshotTimestamp() { return this.selfSponsorshipAlgoV1SnapshotTimestamp; @@ -653,6 +681,22 @@ public class BlockChain { if (totalShareV2 < 0 || totalShareV2 > 1_00000000L) Settings.throwValidationError("Total non-founder share out of bounds (0 this.blockRewardBatchSize) + Settings.throwValidationError("\"blockRewardBatchAccountsBlockCount\" must be less than or equal to \"blockRewardBatchSize\""); } /** Minor normalization, cached value generation, etc. */ diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 35c89778..15fe34ec 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -23,6 +23,7 @@ import org.qortal.data.account.RewardShareData; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; import org.qortal.data.block.CommonBlockData; +import org.qortal.data.network.OnlineAccountData; import org.qortal.data.transaction.TransactionData; import org.qortal.network.Network; import org.qortal.network.Peer; @@ -36,6 +37,7 @@ import org.qortal.utils.Base58; import org.qortal.utils.NTP; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; // Minting new blocks @@ -126,10 +128,6 @@ public class BlockMinter extends Thread { if (minLatestBlockTimestamp == null) continue; - // No online accounts for current timestamp? (e.g. during startup) - if (!OnlineAccountsManager.getInstance().hasOnlineAccounts()) - continue; - List mintingAccountsData = repository.getAccountRepository().getMintingAccounts(); // No minting accounts? if (mintingAccountsData.isEmpty()) @@ -545,6 +543,18 @@ public class BlockMinter extends Thread { return mintTestingBlockRetainingTimestamps(repository, mintingAccount); } + public static Block mintTestingBlockUnvalidatedWithoutOnlineAccounts(Repository repository, PrivateKeyAccount mintingAccount) throws DataException { + if (!BlockChain.getInstance().isTestChain()) + throw new DataException("Ignoring attempt to mint testing block for non-test chain!"); + + // Make sure there are no online accounts + OnlineAccountsManager.getInstance().removeAllOnlineAccounts(); + List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(); + assertTrue(onlineAccounts.isEmpty()); + + return mintTestingBlockRetainingTimestamps(repository, mintingAccount); + } + public static Block mintTestingBlockRetainingTimestamps(Repository repository, PrivateKeyAccount mintingAccount) throws DataException { BlockData previousBlockData = repository.getBlockRepository().getLastBlock(); diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java index 25cace2f..25ba49bc 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -778,6 +778,13 @@ public class OnlineAccountsManager { } + // Utils + + public void removeAllOnlineAccounts() { + this.currentOnlineAccounts.clear(); + } + + // Network handlers public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) { diff --git a/src/main/java/org/qortal/data/block/BlockData.java b/src/main/java/org/qortal/data/block/BlockData.java index 763bca45..3aff34bf 100644 --- a/src/main/java/org/qortal/data/block/BlockData.java +++ b/src/main/java/org/qortal/data/block/BlockData.java @@ -191,10 +191,18 @@ public class BlockData implements Serializable { return this.encodedOnlineAccounts; } + public void setEncodedOnlineAccounts(byte[] encodedOnlineAccounts) { + this.encodedOnlineAccounts = encodedOnlineAccounts; + } + public int getOnlineAccountsCount() { return this.onlineAccountsCount; } + public void setOnlineAccountsCount(int onlineAccountsCount) { + this.onlineAccountsCount = onlineAccountsCount; + } + public Long getOnlineAccountsTimestamp() { return this.onlineAccountsTimestamp; } diff --git a/src/main/java/org/qortal/repository/BlockRepository.java b/src/main/java/org/qortal/repository/BlockRepository.java index 76891c36..59abd0ae 100644 --- a/src/main/java/org/qortal/repository/BlockRepository.java +++ b/src/main/java/org/qortal/repository/BlockRepository.java @@ -132,6 +132,17 @@ public interface BlockRepository { */ public List getBlocks(int firstBlockHeight, int lastBlockHeight) throws DataException; + /** + * Returns blocks within height range. + */ + public Long getTotalFeesInBlockRange(int firstBlockHeight, int lastBlockHeight) throws DataException; + + /** + * Returns block with highest online accounts count in specified range. If more than one block + * has the same high count, the oldest one is returned. + */ + public BlockData getBlockInRangeWithHighestOnlineAccountsCount(int firstBlockHeight, int lastBlockHeight) throws DataException; + /** * Returns block summaries for the passed height range. */ diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java index f38d549c..9f246b35 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java @@ -357,6 +357,36 @@ public class HSQLDBBlockRepository implements BlockRepository { } } + @Override + public Long getTotalFeesInBlockRange(int firstBlockHeight, int lastBlockHeight) throws DataException { + String sql = "SELECT SUM(total_fees) AS sum_total_fees FROM Blocks WHERE height BETWEEN ? AND ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, firstBlockHeight, lastBlockHeight)) { + if (resultSet == null) + return null; + + long totalFees = resultSet.getLong(1); + + return totalFees; + } catch (SQLException e) { + throw new DataException("Error fetching total fees in block range from repository", e); + } + } + + @Override + public BlockData getBlockInRangeWithHighestOnlineAccountsCount(int firstBlockHeight, int lastBlockHeight) throws DataException { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height BETWEEN ? AND ? " + + "ORDER BY online_accounts_count DESC, height ASC " + + "LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, firstBlockHeight, lastBlockHeight)) { + return getBlockFromResultSet(resultSet); + + } catch (SQLException e) { + throw new DataException("Error fetching highest online accounts block in range from repository", e); + } + } + @Override public List getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException { String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count, reference " diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index bdff9506..6ec258d0 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -941,7 +941,9 @@ public class Settings { } public int getPruneBlockLimit() { - return this.pruneBlockLimit; + // Never prune more than twice the block reward batch size, as the data is needed when processing/orphaning + int minPruneBlockLimit = BlockChain.getInstance().getBlockRewardBatchSize() * 2; + return Math.max(this.pruneBlockLimit, minPruneBlockLimit); } public long getAtStatesPruneInterval() { diff --git a/src/main/java/org/qortal/transform/block/BlockTransformer.java b/src/main/java/org/qortal/transform/block/BlockTransformer.java index 15445327..a6d254e3 100644 --- a/src/main/java/org/qortal/transform/block/BlockTransformer.java +++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java @@ -460,6 +460,10 @@ public class BlockTransformer extends Transformer { } public static ConciseSet decodeOnlineAccounts(byte[] encodedOnlineAccounts) { + if (encodedOnlineAccounts.length == 0) { + return new ConciseSet(); + } + int[] words = new int[encodedOnlineAccounts.length / 4]; ByteBuffer.wrap(encodedOnlineAccounts).asIntBuffer().get(words); return new ConciseSet(words, false); diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 9a26c99e..c15aaffe 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -30,6 +30,9 @@ "onlineAccountsModulusV2Timestamp": 1659801600000, "selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000, "mempowTransactionUpdatesTimestamp": 1693558800000, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 1000, + "blockRewardBatchAccountsBlockCount": 25, "rewardsByHeight": [ { "height": 1, "reward": 5.00 }, { "height": 259201, "reward": 4.75 }, diff --git a/src/test/java/org/qortal/test/common/AccountUtils.java b/src/test/java/org/qortal/test/common/AccountUtils.java index bdfd124b..014663d3 100644 --- a/src/test/java/org/qortal/test/common/AccountUtils.java +++ b/src/test/java/org/qortal/test/common/AccountUtils.java @@ -1,13 +1,16 @@ package org.qortal.test.common; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.qortal.crypto.Qortal25519Extras.signForAggregation; import java.security.SecureRandom; import java.util.*; import com.google.common.primitives.Longs; +import org.qortal.account.Account; import org.qortal.account.PrivateKeyAccount; +import org.qortal.account.PublicKeyAccount; import org.qortal.crypto.Crypto; import org.qortal.crypto.Qortal25519Extras; import org.qortal.data.network.OnlineAccountData; @@ -107,6 +110,12 @@ public class AccountUtils { return sponsees; } + public static Account createRandomAccount(Repository repository) { + byte[] randomPublicKey = new byte[32]; + new Random().nextBytes(randomPublicKey); + return new PublicKeyAccount(repository, randomPublicKey); + } + public static Transaction.ValidationResult createRandomRewardShare(Repository repository, PrivateKeyAccount account) throws DataException { // Bob attempts to create a reward share transaction byte[] randomPrivateKey = new byte[32]; @@ -172,6 +181,24 @@ public class AccountUtils { assertEquals(String.format("%s's %s [%d] balance incorrect", accountName, assetName, assetId), expectedBalance, actualBalance); } + public static void assertBalanceGreaterThan(Repository repository, String accountName, long assetId, long minimumBalance) throws DataException { + long actualBalance = getBalance(repository, accountName, assetId); + String assetName = repository.getAssetRepository().fromAssetId(assetId).getName(); + + assertTrue(String.format("%s's %s [%d] balance incorrect", accountName, assetName, assetId), actualBalance > minimumBalance); + } + + + public static int getBlocksMinted(Repository repository, String accountName) throws DataException { + return Common.getTestAccount(repository, accountName).getBlocksMinted(); + } + + public static void assertBlocksMinted(Repository repository, String accountName, int expectedBlocksMinted) throws DataException { + int actualBlocksMinted = getBlocksMinted(repository, accountName); + + assertEquals(String.format("%s's blocks minted incorrect", accountName), expectedBlocksMinted, actualBlocksMinted); + } + public static List generateOnlineAccounts(int numAccounts) { List onlineAccounts = new ArrayList<>(); diff --git a/src/test/java/org/qortal/test/common/BlockUtils.java b/src/test/java/org/qortal/test/common/BlockUtils.java index ab57dadf..fdc74e6c 100644 --- a/src/test/java/org/qortal/test/common/BlockUtils.java +++ b/src/test/java/org/qortal/test/common/BlockUtils.java @@ -10,6 +10,8 @@ import org.qortal.data.block.BlockData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import static org.junit.Assert.*; + public class BlockUtils { private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class); @@ -29,6 +31,20 @@ public class BlockUtils { return block; } + /** Mints a new block using "alice-reward-share" test account, via multiple re-orgs. */ + public static Block mintBlockWithReorgs(Repository repository, int reorgCount) throws DataException { + PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share"); + Block block; + + for (int i=0; i> initialBalances = AccountUtils.getBalances(repository, Asset.QORT); + + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + + Long blockReward = BlockUtils.getNextBlockReward(repository); + + // Deploy an AT so we have transaction fees in each block + // This also mints block 2 + DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, Common.getTestAccount(repository, "bob"), AtUtils.buildSimpleAT(), 1_00000000L); + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 2); + + long expectedBalance = initialBalances.get("alice").get(Asset.QORT) + blockReward + deployAtTransaction.getTransactionData().getFee(); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance); + long aliceCurrentBalance = expectedBalance; + + AccountUtils.assertBlocksMinted(repository, "alice", 1); + + // Mint blocks 3-20 + Block block; + for (int i=3; i<=20; i++) { + expectedBalance = aliceCurrentBalance + BlockUtils.getNextBlockReward(repository); + block = BlockUtils.mintBlockWithReorgs(repository, 10); + expectedBalance += block.getBlockData().getTotalFees(); + assertFalse(block.isBatchRewardDistributionActive()); + assertTrue(block.isRewardDistributionBlock()); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance); + aliceCurrentBalance = expectedBalance; + } + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 20); + + AccountUtils.assertBlocksMinted(repository, "alice", 19); + + // Mint blocks 21-29 + long expectedFees = 0L; + for (int i=21; i<=29; i++) { + + // Create payment transaction so that an additional fee is added to the next block + Account recipient = AccountUtils.createRandomAccount(repository); + TransactionData paymentTransactionData = new PaymentTransactionData(TestTransaction.generateBase(bob), recipient.getAddress(), 100000L); + TransactionUtils.signAndImportValid(repository, paymentTransactionData, bob); + + block = BlockUtils.mintBlockWithReorgs(repository, 8); + expectedFees += block.getBlockData().getTotalFees(); + + // Batch distribution now active + assertTrue(block.isBatchRewardDistributionActive()); + + // It's not a distribution block because we haven't reached the batch size yet + assertFalse(block.isRewardDistributionBlock()); + } + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 29); + + AccountUtils.assertBlocksMinted(repository, "alice", 19); + + // No payouts since block 20 due to batching (to be paid at block 30) + AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance); + + // Block reward to be used for next batch payout + blockReward = BlockUtils.getNextBlockReward(repository); + + // Mint block 30 + block = BlockUtils.mintBlockWithReorgs(repository, 9); + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 30); + + expectedFees += block.getBlockData().getTotalFees(); + assertTrue(expectedFees > 0); + + AccountUtils.assertBlocksMinted(repository, "alice", 29); + + // Batch distribution still active + assertTrue(block.isBatchRewardDistributionActive()); + + // It's a distribution block + assertTrue(block.isRewardDistributionBlock()); + + // Balance should increase by the block reward multiplied by the batch size + expectedBalance = aliceCurrentBalance + (blockReward * BlockChain.getInstance().getBlockRewardBatchSize()) + expectedFees; + AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance); + + // Mint blocks 31-39 + for (int i=31; i<=39; i++) { + block = BlockUtils.mintBlockWithReorgs(repository, 13); + + // Batch distribution still active + assertTrue(block.isBatchRewardDistributionActive()); + + // It's not a distribution block because we haven't reached the batch size yet + assertFalse(block.isRewardDistributionBlock()); + } + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 39); + + AccountUtils.assertBlocksMinted(repository, "alice", 29); + + // No payouts since block 30 due to batching (to be paid at block 40) + AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance); + + // Batch distribution still active + assertTrue(block.isBatchRewardDistributionActive()); + + // It's not a distribution block + assertFalse(block.isRewardDistributionBlock()); + } + } + + @Test + public void testBatchRewardOnlineAccounts() throws DataException, IllegalAccessException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, 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"); + PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share"); + + // Create self shares for bob, chloe and dilbert + AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert)); + + // Mint blocks 2-6 + for (int i=2; i<=6; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 5); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Mint block 7 + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare); + Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(2, block7.getBlockData().getOnlineAccountsCount()); + + // Mint block 8 + onlineAccounts = Arrays.asList(aliceSelfShare, chloeSelfShare); + Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(2, block8.getBlockData().getOnlineAccountsCount()); + + // Mint block 9 + onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare); + Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block9.getBlockData().getOnlineAccountsCount()); + + // Mint block 10 + Block block10 = BlockUtils.mintBlockWithReorgs(repository, 11); + + // Online accounts should be included from block 8 + assertEquals(3, block10.getBlockData().getOnlineAccountsCount()); + + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10); + + // It's a distribution block + assertTrue(block10.isBatchRewardDistributionBlock()); + } + } + + @Test + public void testBatchReward1000Blocks() throws DataException, IllegalAccessException { + // Set reward batching to every 1000 blocks, starting at block 1000, looking back the last 25 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 1000, 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 2-1000 - these should be regular non-batched reward distribution blocks + for (int i=2; i<=1000; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 2); + assertFalse(block.isBatchRewardDistributionActive()); + assertTrue(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertTrue(block.isOnlineAccountsBlock()); + } + + // Mint blocks 1001-1974 - these should have no online accounts or rewards + for (int i=1001; i<=1974; 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 1975-1999 - these should have online accounts but no rewards + for (int i=1975; i<=1998; i++) { + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertTrue(block.isOnlineAccountsBlock()); + assertEquals(2, block.getBlockData().getOnlineAccountsCount()); + } + + // Mint block 1999 - same as above, but with more online accounts + 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 2000 + Block block2000 = BlockUtils.mintBlockWithReorgs(repository, 12); + + // Online accounts should be included from block 1999 + assertEquals(3, block2000.getBlockData().getOnlineAccountsCount()); + + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 2000); + + // It's a distribution block (which is technically also an online accounts block) + assertTrue(block2000.isBatchRewardDistributionBlock()); + assertTrue(block2000.isRewardDistributionBlock()); + assertTrue(block2000.isBatchRewardDistributionActive()); + assertTrue(block2000.isOnlineAccountsBlock()); + } + } + + @Test + public void testBatchRewardHighestOnlineAccountsCount() throws DataException, IllegalAccessException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, 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"); + PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share"); + + // Create self shares for bob, chloe and dilbert + AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert)); + + // Mint blocks 2-6 + for (int i=2; i<=6; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 3); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Capture initial balances now that the online accounts test is ready to begin + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT); + + // Mint block 7 + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare); + Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(2, block7.getBlockData().getOnlineAccountsCount()); + + // Mint block 8 + onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block8.getBlockData().getOnlineAccountsCount()); + + // Mint block 9 + onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare); + Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block9.getBlockData().getOnlineAccountsCount()); + + // Mint block 10 + Block block10 = BlockUtils.mintBlockWithReorgs(repository, 7); + + // Online accounts should be included from block 8 + assertEquals(3, block10.getBlockData().getOnlineAccountsCount()); + + // Dilbert's balance should remain the same as he wasn't included in block 8 + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, initialBalances.get("dilbert").get(Asset.QORT)); + + // Alice, Bob, and Chloe's balances should have increased, as they were all included in block 8 (and therefore block 10) + AccountUtils.assertBalanceGreaterThan(repository, "alice", Asset.QORT, initialBalances.get("alice").get(Asset.QORT)); + AccountUtils.assertBalanceGreaterThan(repository, "bob", Asset.QORT, initialBalances.get("bob").get(Asset.QORT)); + AccountUtils.assertBalanceGreaterThan(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT)); + + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10); + + // It's a distribution block + assertTrue(block10.isBatchRewardDistributionBlock()); + } + } + + @Test + public void testBatchRewardNoOnlineAccounts() throws DataException, IllegalAccessException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + + // Mint blocks 2-6 with no online accounts + for (int i=2; i<=6; i++) { + Block block = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare); + assertNotNull("Minted block must not be null", block); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Mint block 7 with no online accounts + Block block7 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare); + assertNull("Minted block must be null", block7); + + // Mint block 7, this time with an online account + List onlineAccounts = Arrays.asList(aliceSelfShare); + block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertNotNull("Minted block must not be null", block7); + assertEquals(1, block7.getBlockData().getOnlineAccountsCount()); + + // Mint block 8 with no online accounts + Block block8 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare); + assertNull("Minted block must be null", block8); + + // Mint block 8, this time with an online account + block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertNotNull("Minted block must not be null", block8); + assertEquals(1, block8.getBlockData().getOnlineAccountsCount()); + + // Mint block 9 with no online accounts + Block block9 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare); + assertNull("Minted block must be null", block9); + + // Mint block 9, this time with an online account + block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertNotNull("Minted block must not be null", block9); + assertEquals(1, block9.getBlockData().getOnlineAccountsCount()); + + // Mint block 10 + Block block10 = BlockUtils.mintBlockWithReorgs(repository, 8); + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10); + + // It's a distribution block + assertTrue(block10.isBatchRewardDistributionBlock()); + } + } + + @Test + public void testMissingOnlineAccountsInDistributionBlock() throws DataException, IllegalAccessException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, 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 2-6 + for (int i=2; i<=6; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 9); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Mint blocks 7-9 + for (int i=7; i<=9; i++) { + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + } + + // Mint block 10 + Block block10 = Block.mint(repository, repository.getBlockRepository().getLastBlock(), aliceSelfShare); + assertNotNull(block10); + + // Remove online accounts (incorrect as there should be 3) + block10.getBlockData().setEncodedOnlineAccounts(new byte[0]); + + block10.sign(); + block10.clearOnlineAccountsValidationCache(); + + // Must be invalid because online accounts don't match + assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid()); + } + } + + @Test + public void testSignaturesIncludedInDistributionBlock() throws DataException, IllegalAccessException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, 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 2-6 + for (int i=2; i<=6; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 4); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Mint blocks 7-9 + for (int i=7; i<=9; i++) { + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + } + + // Mint block 10 + BlockData previousBlock = repository.getBlockRepository().getLastBlock(); + Block block10 = Block.mint(repository, previousBlock, aliceSelfShare); + assertNotNull(block10); + + // Include online accounts signatures + block10.getBlockData().setOnlineAccountsSignatures(previousBlock.getOnlineAccountsSignatures()); + + block10.sign(); + block10.clearOnlineAccountsValidationCache(); + + // Must be invalid because signatures aren't allowed to be included + assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid()); + } + } + + @Test + public void testOnlineAccountsTimestampIncludedInDistributionBlock() throws DataException, IllegalAccessException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, 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 2-6 + for (int i=2; i<=6; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 6); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Mint blocks 7-9 + for (int i=7; i<=9; i++) { + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + } + + // Mint block 10 + BlockData previousBlock = repository.getBlockRepository().getLastBlock(); + Block block10 = Block.mint(repository, previousBlock, aliceSelfShare); + assertNotNull(block10); + + // Include online accounts timestamp + block10.getBlockData().setOnlineAccountsTimestamp(previousBlock.getOnlineAccountsTimestamp()); + + block10.sign(); + block10.clearOnlineAccountsValidationCache(); + + // Must be invalid because timestamp isn't allowed to be included + assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid()); + } + } + + @Test + public void testIncorrectOnlineAccountsCountInDistributionBlock() throws DataException, IllegalAccessException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, 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 2-6 + for (int i=2; i<=6; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 5); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Mint blocks 7-9 + for (int i=7; i<=9; i++) { + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + } + + // Mint block 10 + BlockData previousBlock = repository.getBlockRepository().getLastBlock(); + Block block10 = Block.mint(repository, previousBlock, aliceSelfShare); + assertNotNull(block10); + + // Update online accounts count so that it is incorrect + block10.getBlockData().setOnlineAccountsCount(10); + + block10.sign(); + block10.clearOnlineAccountsValidationCache(); + + // Must be invalid because online accounts count is incorrect + assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid()); + } + } + + @Test + public void testBatchRewardBlockSerialization() throws DataException, IllegalAccessException, TransformationException { + // Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, 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"); + PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share"); + + // Create self shares for bob, chloe and dilbert + AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert)); + + // Mint blocks 2-6 + Block block = null; + for (int i=2; i<=6; i++) { + block = BlockUtils.mintBlockWithReorgs(repository, 7); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + } + + // Test serialising and deserializing a block with no online accounts + BlockData block6Data = block.getBlockData(); + byte[] block6Bytes = BlockTransformer.toBytes(block); + BlockData block6DataDeserialized = BlockTransformer.fromBytes(block6Bytes).getBlockData(); + BlockUtils.assertEqual(block6Data, block6DataDeserialized); + + // Capture initial balances now that the online accounts test is ready to begin + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT); + + // Mint block 7 + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare); + Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(2, block7.getBlockData().getOnlineAccountsCount()); + + // Mint block 8 + onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block8.getBlockData().getOnlineAccountsCount()); + + // Mint block 9 + onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare); + Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(3, block9.getBlockData().getOnlineAccountsCount()); + + // Mint block 10 + Block block10 = BlockUtils.mintBlockWithReorgs(repository, 15); + + // Online accounts should be included from block 8 + assertEquals(3, block10.getBlockData().getOnlineAccountsCount()); + + // Dilbert's balance should remain the same as he wasn't included in block 8 + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, initialBalances.get("dilbert").get(Asset.QORT)); + + // Alice, Bob, and Chloe's balances should have increased, as they were all included in block 8 (and therefore block 10) + AccountUtils.assertBalanceGreaterThan(repository, "alice", Asset.QORT, initialBalances.get("alice").get(Asset.QORT)); + AccountUtils.assertBalanceGreaterThan(repository, "bob", Asset.QORT, initialBalances.get("bob").get(Asset.QORT)); + AccountUtils.assertBalanceGreaterThan(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT)); + + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10); + + // It's a distribution block + assertTrue(block10.isBatchRewardDistributionBlock()); + } + } + +} diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 7059e035..2d3a6484 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -19,6 +19,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 1016bc17..30691293 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 5f29bc97..9b273323 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 86f2def1..e7339947 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 b2da6489..fe03c37d 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 9999999999999, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 2933a63d..8acc0a35 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 40e40673..642c6415 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 8ceafe63..aa1a23f3 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 68a79ed4..3073dfa9 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 cc02a73e..d602de18 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 5c508188..1261be0d 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 244d2491..a63a395f 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo.json @@ -24,6 +24,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "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 9168a0de..2a7aa362 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -25,6 +25,9 @@ "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, From 15eefa4177cfe1140876a36754b1b5c4ae57bed7 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 6 Oct 2023 11:03:06 +0100 Subject: [PATCH 2/3] Improved batch reward logging and moved it to debug level --- src/main/java/org/qortal/block/Block.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 9d40d8da..aca2d052 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1678,21 +1678,19 @@ public class Block { // Add transaction fees since last distribution block int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchSize() + 1; int lastBlock = this.blockData.getHeight() - 1; - Long startTime = NTP.getTime(); Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock); - LOGGER.info("[process] Fetching total fees took {} ms", (NTP.getTime()-startTime)); if (totalFees == null) { throw new DataException("Unable to calculate total fees for block range"); } reward += totalFees; - LOGGER.info("[process] Total fees for range {} - {}: {}", firstBlock, lastBlock, totalFees); + LOGGER.debug("Total fees for range {} - {} when processing: {}", firstBlock, lastBlock, totalFees); } // Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet) reward += this.blockData.getTotalFees(); - LOGGER.info("[process] Total fees for block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); + LOGGER.debug("Total fees when processing block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); - LOGGER.info("[process] Block reward for height {}: {}", this.blockData.getHeight(), reward); + LOGGER.debug("Block reward when processing block {}: {}", this.blockData.getHeight(), reward); // Nothing to reward? if (reward <= 0) @@ -1951,14 +1949,14 @@ public class Block { throw new DataException("Unable to calculate total fees for block range"); } reward += totalFees; - LOGGER.info("[orphan] Total fees for range {} - {}: {}", firstBlock, lastBlock, totalFees); + LOGGER.debug("Total fees for range {} - {} when orphaning: {}", firstBlock, lastBlock, totalFees); } // Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet) reward += this.blockData.getTotalFees(); - LOGGER.info("[orphan] Total fees for block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); + LOGGER.debug("Total fees when orphaning block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); - LOGGER.info("[orphan] Block reward for height {}: {}", this.blockData.getHeight(), reward); + LOGGER.debug("Block reward when orphaning block {}: {}", this.blockData.getHeight(), reward); // Nothing to reward? if (reward <= 0) From 9e8e0df5d6805aeabfa4156495ccf69d1072a942 Mon Sep 17 00:00:00 2001 From: AlphaX-Projects <77661270+AlphaX-Projects@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:53:25 +0100 Subject: [PATCH 3/3] Set batch payout blockheight --- src/main/resources/blockchain.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index f4010711..2fc69347 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -31,7 +31,7 @@ "onlineAccountsModulusV2Timestamp": 1659801600000, "selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000, "mempowTransactionUpdatesTimestamp": 1693558800000, - "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchStartHeight": 1508000, "blockRewardBatchSize": 1000, "blockRewardBatchAccountsBlockCount": 25, "rewardsByHeight": [