diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 51d55eb4..41faf51b 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -370,83 +370,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, @@ -1058,6 +1082,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; @@ -1477,11 +1535,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 @@ -1541,9 +1603,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<>(); @@ -1593,8 +1659,32 @@ 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 totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock); + if (totalFees == null) { + throw new DataException("Unable to calculate total fees for block range"); + } + reward += 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.debug("Total fees when processing block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); + + LOGGER.debug("Block reward when processing block {}: {}", this.blockData.getHeight(), reward); // Nothing to reward? if (reward <= 0) @@ -1752,11 +1842,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 @@ -1832,8 +1925,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.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.debug("Total fees when orphaning block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees()); + + LOGGER.debug("Block reward when orphaning block {}: {}", this.blockData.getHeight(), reward); // Nothing to reward? if (reward <= 0) @@ -1874,9 +1991,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) @@ -1899,6 +2020,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 4dcc148d..aa2ab9bb 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -211,6 +211,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; @@ -367,6 +380,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; @@ -652,6 +680,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 15660f97..15bcb1d7 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -12,6 +12,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 java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; 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 17501a75..9518b7f3 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -780,6 +780,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 ffae8751..34df0f9a 100644 --- a/src/main/java/org/qortal/data/block/BlockData.java +++ b/src/main/java/org/qortal/data/block/BlockData.java @@ -189,10 +189,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 429e34cb..e8a1222a 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 f592a79d..c15dcfb1 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java @@ -356,6 +356,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 d7b28fb5..ba90208b 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -1017,7 +1017,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 fee585ca..fd886293 100644 --- a/src/main/java/org/qortal/transform/block/BlockTransformer.java +++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java @@ -458,6 +458,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 760da97c..2fc69347 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -31,6 +31,9 @@ "onlineAccountsModulusV2Timestamp": 1659801600000, "selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000, "mempowTransactionUpdatesTimestamp": 1693558800000, + "blockRewardBatchStartHeight": 1508000, + "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 e19cffff..80719339 100644 --- a/src/test/java/org/qortal/test/common/AccountUtils.java +++ b/src/test/java/org/qortal/test/common/AccountUtils.java @@ -1,7 +1,9 @@ package org.qortal.test.common; 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; @@ -20,6 +22,7 @@ import java.security.SecureRandom; import java.util.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.qortal.crypto.Qortal25519Extras.signForAggregation; public class AccountUtils { @@ -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 },