mirror of
				https://github.com/Qortal/qortal.git
				synced 2025-11-04 05:47:03 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			v4.3.1
			...
			batch-rewa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					15eefa4177 | ||
| 
						 | 
					8b69b65712 | 
@@ -376,7 +376,26 @@ public class Block {
 | 
			
		||||
 | 
			
		||||
		int height = parentBlockData.getHeight() + 1;
 | 
			
		||||
		long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
 | 
			
		||||
		long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
 | 
			
		||||
 | 
			
		||||
		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
 | 
			
		||||
 | 
			
		||||
			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
 | 
			
		||||
 | 
			
		||||
			// Fetch our list of online accounts, removing any that are missing a nonce
 | 
			
		||||
			List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
 | 
			
		||||
@@ -420,8 +439,8 @@ public class Block {
 | 
			
		||||
			// Convert to compressed integer set
 | 
			
		||||
			ConciseSet onlineAccountsSet = new ConciseSet();
 | 
			
		||||
			onlineAccountsSet = onlineAccountsSet.convert(accountIndexes);
 | 
			
		||||
		byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
 | 
			
		||||
		int onlineAccountsCount = onlineAccountsSet.size();
 | 
			
		||||
			encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
 | 
			
		||||
			onlineAccountsCount = onlineAccountsSet.size();
 | 
			
		||||
 | 
			
		||||
			// Collate all signatures
 | 
			
		||||
			Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
 | 
			
		||||
@@ -430,7 +449,7 @@ public class Block {
 | 
			
		||||
					.collect(Collectors.toList());
 | 
			
		||||
 | 
			
		||||
			// Aggregated, single signature
 | 
			
		||||
		byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
 | 
			
		||||
			onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
 | 
			
		||||
 | 
			
		||||
			// Add nonces to the end of the online accounts signatures
 | 
			
		||||
			try {
 | 
			
		||||
@@ -450,11 +469,16 @@ public class Block {
 | 
			
		||||
				outputStream.write(onlineAccountsSignatures);
 | 
			
		||||
				outputStream.write(encodedNonces);
 | 
			
		||||
				onlineAccountsSignatures = outputStream.toByteArray();
 | 
			
		||||
		}
 | 
			
		||||
		catch (TransformationException | IOException e) {
 | 
			
		||||
			} 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,
 | 
			
		||||
				minter.getPublicKey(), encodedOnlineAccounts));
 | 
			
		||||
 | 
			
		||||
@@ -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<RewardShareData> 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) {
 | 
			
		||||
 | 
			
		||||
			// 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<String, Integer> bumpedAccounts = new HashMap<>();
 | 
			
		||||
@@ -1599,8 +1665,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)
 | 
			
		||||
@@ -1758,12 +1848,15 @@ public class Block {
 | 
			
		||||
			else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
 | 
			
		||||
				SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
 | 
			
		||||
 | 
			
		||||
			// 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();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Delete block from blockchain
 | 
			
		||||
		this.repository.getBlockRepository().delete(this.blockData);
 | 
			
		||||
@@ -1838,8 +1931,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)
 | 
			
		||||
@@ -1880,9 +1997,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 +2026,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<x<1e8)");
 | 
			
		||||
 | 
			
		||||
		// Check that blockRewardBatchSize isn't zero
 | 
			
		||||
		if (this.blockRewardBatchSize <= 0)
 | 
			
		||||
			Settings.throwValidationError("\"blockRewardBatchSize\" must be greater than 0");
 | 
			
		||||
 | 
			
		||||
		// Check that blockRewardBatchStartHeight is a multiple of blockRewardBatchSize
 | 
			
		||||
		if (this.blockRewardBatchStartHeight % this.blockRewardBatchSize != 0)
 | 
			
		||||
			Settings.throwValidationError("\"blockRewardBatchStartHeight\" must be a multiple of \"blockRewardBatchSize\"");
 | 
			
		||||
 | 
			
		||||
		// Check that blockRewardBatchAccountsBlockCount isn't zero
 | 
			
		||||
		if (this.blockRewardBatchAccountsBlockCount <= 0)
 | 
			
		||||
			Settings.throwValidationError("\"blockRewardBatchAccountsBlockCount\" must be greater than 0");
 | 
			
		||||
 | 
			
		||||
		// Check that blockRewardBatchSize isn't zero
 | 
			
		||||
		if (this.blockRewardBatchAccountsBlockCount > this.blockRewardBatchSize)
 | 
			
		||||
			Settings.throwValidationError("\"blockRewardBatchAccountsBlockCount\" must be less than or equal to \"blockRewardBatchSize\"");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Minor normalization, cached value generation, etc. */
 | 
			
		||||
 
 | 
			
		||||
@@ -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<MintingAccountData> 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<OnlineAccountData> 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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -778,6 +778,13 @@ public class OnlineAccountsManager {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Utils
 | 
			
		||||
 | 
			
		||||
    public void removeAllOnlineAccounts() {
 | 
			
		||||
        this.currentOnlineAccounts.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Network handlers
 | 
			
		||||
 | 
			
		||||
    public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,17 @@ public interface BlockRepository {
 | 
			
		||||
	 */
 | 
			
		||||
	public List<BlockData> 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.
 | 
			
		||||
	 */
 | 
			
		||||
 
 | 
			
		||||
@@ -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<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
 | 
			
		||||
		String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count, reference "
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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<OnlineAccountData> generateOnlineAccounts(int numAccounts) {
 | 
			
		||||
		List<OnlineAccountData> onlineAccounts = new ArrayList<>();
 | 
			
		||||
 
 | 
			
		||||
@@ -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<reorgCount; i++) {
 | 
			
		||||
			block = BlockMinter.mintTestingBlock(repository, mintingAccount);
 | 
			
		||||
			assertNotNull(block);
 | 
			
		||||
			BlockUtils.orphanLastBlock(repository);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return BlockMinter.mintTestingBlock(repository, mintingAccount);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static Long getNextBlockReward(Repository repository) throws DataException {
 | 
			
		||||
		int currentHeight = repository.getBlockRepository().getBlockchainHeight();
 | 
			
		||||
 | 
			
		||||
@@ -70,4 +86,23 @@ public class BlockUtils {
 | 
			
		||||
		} while (true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static void assertEqual(BlockData block1, BlockData block2) {
 | 
			
		||||
		assertArrayEquals(block1.getSignature(), block2.getSignature());
 | 
			
		||||
		assertEquals(block1.getVersion(), block2.getVersion());
 | 
			
		||||
		assertArrayEquals(block1.getReference(), block2.getReference());
 | 
			
		||||
		assertEquals(block1.getTransactionCount(), block2.getTransactionCount());
 | 
			
		||||
		assertEquals(block1.getTotalFees(), block2.getTotalFees());
 | 
			
		||||
		assertArrayEquals(block1.getTransactionsSignature(), block2.getTransactionsSignature());
 | 
			
		||||
		// assertEquals(block1.getHeight(), block2.getHeight()); // Height not automatically included after deserialization
 | 
			
		||||
		assertEquals(block1.getTimestamp(), block2.getTimestamp());
 | 
			
		||||
		assertArrayEquals(block1.getMinterPublicKey(), block2.getMinterPublicKey());
 | 
			
		||||
		assertArrayEquals(block1.getMinterSignature(), block2.getMinterSignature());
 | 
			
		||||
		assertEquals(block1.getATCount(), block2.getATCount());
 | 
			
		||||
		assertEquals(block1.getATFees(), block2.getATFees());
 | 
			
		||||
		assertArrayEquals(block1.getEncodedOnlineAccounts(), block2.getEncodedOnlineAccounts());
 | 
			
		||||
		assertEquals(block1.getOnlineAccountsCount(), block2.getOnlineAccountsCount());
 | 
			
		||||
		assertEquals(block1.getOnlineAccountsTimestamp(), block2.getOnlineAccountsTimestamp());
 | 
			
		||||
		assertArrayEquals(block1.getOnlineAccountsSignatures(), block2.getOnlineAccountsSignatures());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										682
									
								
								src/test/java/org/qortal/test/minting/BatchRewardTests.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										682
									
								
								src/test/java/org/qortal/test/minting/BatchRewardTests.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,682 @@
 | 
			
		||||
package org.qortal.test.minting;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.lang3.reflect.FieldUtils;
 | 
			
		||||
import org.junit.After;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.qortal.account.Account;
 | 
			
		||||
import org.qortal.account.PrivateKeyAccount;
 | 
			
		||||
import org.qortal.asset.Asset;
 | 
			
		||||
import org.qortal.block.Block;
 | 
			
		||||
import org.qortal.block.BlockChain;
 | 
			
		||||
import org.qortal.controller.BlockMinter;
 | 
			
		||||
import org.qortal.data.block.BlockData;
 | 
			
		||||
import org.qortal.data.transaction.PaymentTransactionData;
 | 
			
		||||
import org.qortal.data.transaction.TransactionData;
 | 
			
		||||
import org.qortal.repository.DataException;
 | 
			
		||||
import org.qortal.repository.Repository;
 | 
			
		||||
import org.qortal.repository.RepositoryManager;
 | 
			
		||||
import org.qortal.settings.Settings;
 | 
			
		||||
import org.qortal.test.common.*;
 | 
			
		||||
import org.qortal.test.common.transaction.TestTransaction;
 | 
			
		||||
import org.qortal.transaction.DeployAtTransaction;
 | 
			
		||||
import org.qortal.transform.TransformationException;
 | 
			
		||||
import org.qortal.transform.block.BlockTransformer;
 | 
			
		||||
import org.qortal.utils.NTP;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
import static org.junit.Assert.*;
 | 
			
		||||
 | 
			
		||||
public class BatchRewardTests extends Common {
 | 
			
		||||
 | 
			
		||||
	@Before
 | 
			
		||||
	public void beforeTest() throws DataException {
 | 
			
		||||
		Common.useSettings("test-settings-v2-reward-levels.json");
 | 
			
		||||
		NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@After
 | 
			
		||||
	public void afterTest() throws DataException {
 | 
			
		||||
		Common.orphanCheck();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void testBatchReward() throws DataException, IllegalAccessException {
 | 
			
		||||
		// Set reward batching to every 10 blocks, starting at block 20, looking back the last 3 blocks for online accounts
 | 
			
		||||
		FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 20, true);
 | 
			
		||||
		FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
 | 
			
		||||
		FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
 | 
			
		||||
 | 
			
		||||
		try (final Repository repository = RepositoryManager.getRepository()) {
 | 
			
		||||
			Map<String, Map<Long, Long>> 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<PrivateKeyAccount> 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<PrivateKeyAccount> 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<PrivateKeyAccount> 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<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
 | 
			
		||||
 | 
			
		||||
			// Mint block 7
 | 
			
		||||
			List<PrivateKeyAccount> 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<PrivateKeyAccount> 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<PrivateKeyAccount> 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<PrivateKeyAccount> 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<PrivateKeyAccount> 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<PrivateKeyAccount> 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<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
 | 
			
		||||
 | 
			
		||||
			// Mint block 7
 | 
			
		||||
			List<PrivateKeyAccount> 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());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
@@ -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 },
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user